Генерация и валидация чисел по алгоритму Луна

22b3d4beb6581f7022ea4f9d14ec4bcc.png

Алгоритм Луна (Luhn algorithm) — это процесс вычисления контрольной цифры для числа в соответствии со стандартом ISO/IEC 7812. Сам процесс не является криптографическим средством и никак не защищает находящиеся в этом числе данные. Он предназначен, в первую очередь, для выявления ошибок, вызванных с непреднамеренным искажением данных. Например, при ручном вводе номера карты или любого другого числа. Данный алгоритм позволяет с некоторой степенью достоверности судить об отсутствии ошибок в блоке цифр, но никак не может исправить их.

Алгоритм разработан сотрудником IBM Хансом Питером Луном в 1954-м году и запатентован в 1960-м году. Наиболее часто данный алгоритм применяется при формировании номеров всех банковских карт, некоторых номеров дисконтных карт, кодов социального страхования, IMEI-кодов, номеров железнодорожных вагонов РЖД, уникальных серийных номеров SIM-карт (ICCID) и в других случаях.

Проверка числа

Формула очень проста: для того чтобы проверить число в соответствии с алгоритмом Луна, необходимо просуммировать все нечётные цифры последовательности справа налево, далее прибавить к полученному значению сумму всех чётных цифр, умноженных на 2, при этом, если произведение таких чисел больше 9, то из него вычитается 9. Если полученная сумма делится на 10 без остатка, значит начальное число введено верно.

Рассмотрим корректное число »5062 8212 3456 7892»:

5 0 6 2 8 2 1 2 3 4 5 6 7 8 9 2
↓   ↓   ↓   ↓   ↓   ↓   ↓   ↓   // умножаем каждое второе число на 2
10  12  16  2   6   10  14  18
↓   ↓   ↓           ↓   ↓   ↓
1   3   7           1   5   9   // от чисел свыше 9 отнимаем 9
1 0 3 2 7 2 2 2 6 4 1 6 5 8 9 2 // берём все нечётные цифры
                                // и полученный результат чётных
1+0+3+2+7+2+2+2+6+4+1+6+5+8+9+2 = 60

В конечном итоге получаем сумму равную 60. Это число делится на 10 без остатка, значит, номер введён правильно.

Теперь проверим некорректное число »5062 8217 3456 7892»:

5 0 6 2 8 2 1 7 3 4 5 6 7 8 9 2
↓   ↓   ↓   ↓   ↓   ↓   ↓   ↓   // умножаем каждое второе число на 2
10  12  16  2   6   10  14  18
↓   ↓   ↓           ↓   ↓   ↓
1   3   7           1   5   9   // от чисел свыше 9 отнимаем 9
1 0 3 2 7 2 2 7 6 4 1 6 5 8 9 2 // берём все нечётные цифры
                                // и полученный результат чётных
1+0+3+2+7+2+2+7+6+4+1+6+5+8+9+2 = 65

Таким образом мы получили информацию о том, что начальное число неверно написано, а значит необходимо проверить порядок цифр и исправить ошибку.

Пакетное решение на PHP

Пакетных решений по проверки числа по алгоритму Луна много, но редко кто предлагает генерацию таких чисел, и так на свет появился проект The Dragon Code: Card Number.

Данный проект позволяет не только проверять любые числа по алгоритму Луна, но и генерировать их, например, для применения в программе лояльности клиентов.

Установка

Проще всего установить данный продукт можно при помощи Composer:

composer require dragon-code/card-number

И сразу после этого можно приступать к работе.

Валидация

Проверять можно абсолютно любые числа и в любом формате. Например:

use DragonCode\CardNumber\CardNumber;

CardNumber::isValid(18); // true
CardNumber::isValid(12); // false

CardNumber::isValid('0018'); // true
CardNumber::isValid('0019'); // false

CardNumber::isValid('123-455'); // true
CardNumber::isValid('123-454'); // false

CardNumber::isValid('12-3456-1239'); // true
CardNumber::isValid('12-3456-1230'); // false

CardNumber::isValid('5580 4733 7202 4733'); // true
CardNumber::isValid('5580 4733 7202 4732'); // false

CardNumber::isValid('5580-4733x7202_47 33'); // true
CardNumber::isValid('5580-4733x7202_47 32'); // false

Кроме того, помимо валидности самого числа, можно также проверять принадлежность номера какому-либо типу банковских карт. Например:

use DragonCode\CardNumber\CardNumber;
use DragonCode\CardNumber\Enums\CardType;

CardNumber::isValid('4026 8434 8316 8683', CardType::visa); // true
CardNumber::isValid('2730 1684 6416 1841', CardType::visa); // false

CardNumber::isValid('4026 8434 8316 8683', 'visa'); // true
CardNumber::isValid('2730 1684 6416 1841', 'visa'); // false

В настоящий момент проект содержит валидаторы для номеров следующих типов карт: AmericanExpress, Dankort, DinersClub, Discovery, Forbrugsforeningen, HiperCard, JCB, Maestro, MasterCard, МИР, Troy, UnionPay, VISA и VISA Electron.

Генерация номеров

Для генерации номера, например, по идентификатору клиента, можно вызвать следующий код:

use DragonCode\CardNumber\CardNumber;

CardNumber::generate(1);   // 18
CardNumber::generate(2);   // 26
CardNumber::generate(10);  // 109
CardNumber::generate(90);  // 901
CardNumber::generate(908); // 9084

Также возможно применение специального форматирующего класса. Например, для карт лояльности:

use DragonCode\CardNumber\CardNumber;
use DragonCode\CardNumber\Formatters\LoyaltyFormatter;

$loyalty = LoyaltyFormatter::create();
 
CardNumber::generate(1, $loyalty); // 0018
CardNumber::generate(2, $loyalty); // 0026

CardNumber::generate(12345, $loyalty); // 123-455
CardNumber::generate(23456, $loyalty); // 234-567

CardNumber::generate(123456, $loyalty); // 123-4566
CardNumber::generate(234567, $loyalty); // 234-5676

CardNumber::generate(123456123, $loyalty); // 12-3456-1239
CardNumber::generate(234567123, $loyalty); // 23-4567-1230

Или банковских карт:

use DragonCode\CardNumber\CardNumber;
use DragonCode\CardNumber\Formatters\BankFormatter;

$bank = BankFormatter::create();

CardNumber::generate(558047337202473, $bank); // 5580 4733 7202 4733
CardNumber::generate(529391143678555, $bank); // 5293 9114 3678 5557

По-умолчанию доступно три форматтера:

  • DragonCode\CardNumber\Formatters\DefaultFormatter

  • DragonCode\CardNumber\Formatters\BankFormatter

  • DragonCode\CardNumber\Formatters\LoyaltyFormatter

Но Вы с лёгкостью можете создать свой и использовать его. Просто отнаследуйте созданный класс от абстрактного класса »DragonCode\CardNumber\Formatters\Formatter».

Фабрики

При генерации также есть возможность использования фабрик. Это позволит легко и без дополнительного кода сформировать число, для которого необходимо получить контрольное значение по алгоритму Луна.

Например, мы хотим сформировать номер карты лояльности для клиента на основании следующих данных. Допустим, полученное число должно быть длиной ровно 11 символов и общий вид должен иметь формат «xxx-xxxx-xxxx», где:

  • Первые две цифры — год выдачи карты;

  • Третья и четвёртая — уровень или тип лояльности клиента;

  • С пятой по десятую- оставляем под идентификатор пользователя;

  • Одиннадцатая — контрольная цифра.

Будем использовать следующие данные:

  • Год выдачи — 2023

  • Уровень лояльности — 4

  • Идентификатор пользователя — 1234

Для получения числа нам нужно создать фабрику и передать её в параметр идентификатора генератора:

use DragonCode\CardNumber\CardNumber;
use DragonCode\CardNumber\Factories\CustomerFactory;
use DragonCode\CardNumber\Formatters\LoyaltyFormatter;

$formatter = LoyaltyFormatter::create();

$loyaltyLevel = 4;
$userId       = 1234;

$customer = CustomerFactory::create()
    ->level($loyaltyLevel)
    ->customer($userId);

return CardNumber::generate($customer, $formatter);
// 230-4001-2348

Таким образом, на выходе будет получено число »230–4001–2348», которое будет корректным с точки зрения алгоритма Луна.

Изначально проект Card Number содержит фабрики для уровня лояльности клиентов и формирования номеров банковских карт, но Вы можете легко создать свой форматтер по своим условиям. Для этого нужно отнаследовать созданный класс от абстрактного класса »DragonCode\CardNumber\Factories\Factory».

Laravel Framework

Проект Card Number также содержит правило валидации для фреймворка Laravel 10 и выше.

use DragonCode\CardNumber\Laravel\Validation\Rules\CardNumberRule;
use Illuminate\Foundation\Http\FormRequest;

class SomeRequest extends FormRequest
{
    public function rules(): array
    {
        return [
            'number' => ['required', new CardNumberRule()]
        ];
    }
}

Кроме этого можно валидировать конкретный тип или типы карт, передав в параметр конструктора соответствующие данные. Например:

use DragonCode\CardNumber\Enums\CardType;
use DragonCode\CardNumber\Laravel\Validation\Rules\CardNumberRule;
use Illuminate\Foundation\Http\FormRequest;

class SomeRequest extends FormRequest
{
    public function rules(): array
    {
        return [
            'visa_card_1' => ['required', new CardNumberRule(CardType::visa)],
            'visa_card_2' => ['required', new CardNumberRule('visa')],
            
            'few_cards' => ['required', new CardNumberRule([CardType::visa, CardType::masterCard])],
            'few_cards' => ['required', new CardNumberRule(['visa', 'mastercard'])],
        ];
    }
}

По-умолчанию, в случае передачи некорректного типа карты для проверки, правило вернёт сообщение об ошибке с текстом: «The: attribute field must contain a card number of one of the following types:» и перечислит в конце все доступные типы карт для проверки.

В случае если некорректным окажется номер карты, тогда будет возвращено сообщение «The: attribute field must be a valid card number.».

Для локализации этих сообщений Вы можете установить пакет dragon-code/translation-set, который содержит переводы этих сообщений на 78 языков. Данный пакет полностью совместим с проектом Laravel Lang.

© Habrahabr.ru