Алгоритм Луна (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.

Комментарии (28)


  1. GospodinKolhoznik
    01.07.2023 19:34
    +3

    Алгоритм настолько простой, что даже не знаю, что проще, импортировать зависимость или написать нужную функцию самому.


    1. GospodinKolhoznik
      01.07.2023 19:34

      В Хаслеле, например, функцию можно реализовать вот так:

      isValid :: String -> Bool
      isValid = (== 0) . (`mod` 10) . sum . zipWith ($) (cycle [id, \x -> if x < 5 then x*2 else x*2 - 9]) . digits
        where 
          digits = reverse . map (\c -> read [c] )


      1. AnthonyMikh
        01.07.2023 19:34

        \x -> if x < 5 then x*2 else x*2 - 9

        Да проще безусловно брать остаток от деления на 9:


        \x -> (x * 2) `mod` 9


    1. borovichok13
      01.07.2023 19:34
      +1

      Алгоритм-то, простой, а вот программисты кривопишущие.

      1. В номере карты может быть от 14 до 19 цифр. Ряд поддержки шлюзов, считал, что я не умею считать до 16, время исправления ошибки от недели до нескольких месяцев.

      2. Карта МИР может иметь от 16 до 19 цифр. ( у меня с 16 и 19 цифрами).

      3. Генератор номера карты может его сгенерить валидным и по 16й и 19й цифрам - одновременно ( у меня такая была). Последствия были таковыми: платёжный шлюз зелёного банка считал, что раз валидна 16я цифра, то нечего платильщику вводить ещё дополнительные цифры. Не сразу, а случайно узнал про алгоритм Луны. Поддержка работает только в рамках стандартного алгоритма - чтобы передать нужную информацию - это ещё тот квест (поставил поддержке единицу, и адекватный сотрудник позвонил на следующий день). Вроде бы исправили через несколько месяцев.


      1. Helldar Автор
        01.07.2023 19:34

        В ходе работы над проектом выяснил, что:

        • AmericanExpress имеет длину в 15 или 16 символов

        • HiperCard - 13, 16 или 19 символов

        • JCB и UnionPay - от 16 до 19

        • VISA - 13 или 16

        • VISA Electron - 16 или 17

        И самый лютый тип карты из всех - это Maestro - количество символов в её длине может быть от 12 до 19 включительно.

        Не исключая криворукости тех, кто это придумал, такой выбор обусловлен, в том числе, наличием других платёжных систем, когда конечный номер карты должен быть уникальным среди всех банковских систем любой страны, по сути. Вот и придумывают изменение длины.

        "алгоритм Луны" - не Луны, а Луна. Алгоритм разработал Ханс Лун.


  1. mentin
    01.07.2023 19:34
    +5

    все нечётные цифры

    Я бы написал "все цифры на нечётных позициях". А то хоть и понятно что имелось в виду, "нечётные цифры" имеет другое значение.


    1. Helldar Автор
      01.07.2023 19:34
      +2

      Было рассмотрено несколько вариантов этого предложения и каждый раз оно воспринималось двояко. Ваше выглядит лучше. Обновил статью, спасибо!


      1. borovichok13
        01.07.2023 19:34

        А проверка валидности номера карточки из 19 цифр проверяется ли он, что может быть одновременно валидным и для 16 и 19 цифр? Или других комбинаций?


        1. Helldar Автор
          01.07.2023 19:34

          Одновременно нет. Сам алгоритм рассчитывает все цифры, а предложенный пакет отдельно проверяет длину строки: https://github.com/TheDragonCode/card-number/blob/main/src/Cards/Card.php#L20-L32


    1. Didimus
      01.07.2023 19:34
      +1

      Начиная отсечёт слева, с нулевой позиции?


  1. simenoff
    01.07.2023 19:34

    Вместо вычитания 9 можно сложить цифры числа


    1. Helldar Автор
      01.07.2023 19:34

      Можно, но это больше действий.

      Например:

      $number = 18;
      
      $values = str_split((string) $number, 1);
      
      return array_sum($values);

      Или:

      return $number - 9;


  1. AlexeyK77
    01.07.2023 19:34
    +1

    Интересно узнать даже не за имеено этот алгоритм, а мат. аппарат, который лег в его основу.

    Например, если числа будут из другой системы счисления, или может даже это будет просто текст, так какой должен быть алгоритм?


    1. Helldar Автор
      01.07.2023 19:34

      Так как алгоритм использует, по сути, простейшую математику, можно предположить что в любой системе счисления его будет возможно применить. Разница будет лишь во внешнем виде функции-обработчика.

      Что касается текста, то в данном конкретном случае перед обработкой строка очищается от всех символов кроме чисел, а если нужно будет проверять именно буквенную строку, например, "foo-bar-baz", то я бы поступил следующим образом: получил бы для каждой буквы её порядковый номер в алфавите. Если номер больше 9, отнимаем 9 для получения единого числа. Для полученного числа рассчитываем контрольную цифру и на выходе будет готовая строка.

      Например:

      f  o  o  b a r  b a z
      6  15 15 2 1 18 2 1 26
      6  6  6  2 1 9  2 1 8  0 // добавляем для определения контрольной цифры
      ↓     ↓    ↓    ↓   ↓
      12 6  12 2 2 9  4 1 16 0
      ↓     ↓             ↓
      3 +6 +6 +2+2+9 +4+1+7 +0 = 40

      Получаем контрольную цифру "0" так как полученная сумма делится на 10 без остатка. С учётом того, что цифра может быть от 0 до 9 включительно, в качестве "нуля" можно использовать 10-й символ алфавита - "j".

      Таким образом, мы получим верную с точки зрения алгоритма Луна строку foo-bar-baz-j.


      1. AlexeyK77
        01.07.2023 19:34
        +1

        ну, это только предположение, интереснее формальное доказательство, в этом и ыл мой вопрос.

        Согласитесь, что то, что алгоритм кажется простым, не делает его тривиальным. Додуматься до такой числовой манипуляции просто так нельзя, за ней стоит определенный мат. аппарат. Вот это мне и было интересно.
        Но навреное уже и сам поищу, т.к. не в первый раз уже эта тема вызывает любопытство.


  1. anonymous
    01.07.2023 19:34

    НЛО прилетело и опубликовало эту надпись здесь


    1. Helldar Автор
      01.07.2023 19:34

      Статья больше рассчитана на разработчиков, для которых технический язык - родной. Мы же на Хабре, в конце концов. И первое предложение взято из Википедии и чуть упрощено. Оригинал выглядит следующим образом:

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

      https://ru.wikipedia.org/wiki/Алгоритм_Луна

      Вдобавок, статья рассчитана не на объяснение истории появление алгоритма, а на его практическое использование с приведёнными примерами расчёта, а также описанием готового пакетного решения на языке PHP для упрощения генерации и валидации чисел.

      К тому же, совсем необязательно при чтении поста знать о стандарте ISO/IEC 7812. Эта информация скорее не несёт особого смысла, а указано лишь в качестве связанности алгоритма Луна с ним. Для тех кому интересно что за стандарт, легко могут выделить строчку и загуглить его описание. А те, кому это не интересно, просто пропустят этот участок текста.


      1. anonymous
        01.07.2023 19:34

        НЛО прилетело и опубликовало эту надпись здесь


        1. Helldar Автор
          01.07.2023 19:34

          Как скажете.


          1. anonymous
            01.07.2023 19:34

            НЛО прилетело и опубликовало эту надпись здесь


            1. Hungryee
              01.07.2023 19:34
              -2

              Не обращайте внимания, очевидно, что пан Helldar мало того, что полный профан в написании статей и пытается оправдаться «техническим языком», хотя написал чушь; так еще и обиженка на весь мир, раз за подобные комментарии вам минусует

              Статья написана преотвратно, в тексте черт ногу сломит от обилия двояко трактуемых выражений, очень жаль, что боты двигают это вверх


              1. anonymous
                01.07.2023 19:34

                НЛО прилетело и опубликовало эту надпись здесь


  1. Didimus
    01.07.2023 19:34

    А как устроено определение платёжной системы? Там же бины могут перераспределяться


    1. Helldar Автор
      01.07.2023 19:34

      В случае банковских карт платёжные системы применяют формирование номеров карт в соответствии со стандартом ISO/IEC 7812. Это один из международных стандартов, описывающих параметры идентификационных карт и применение таких карт для международного, межотраслевого и/или внутриотраслевого взаимообмена.

      Сам стандарт можно прочитать здесь. Он небольшой, всего 12 листов неполного текста.

      Формирование номеров по стандарту следующее:

      Всегда первые 6 цифр номера - это идентификационный номер эмитента, далее от 4 до 12 цифр - номер лицевого счёта. Последняя цифра - контрольная согласно алгоритму Луна.

      Также в номерах банков первая цифра означает тип платёжной системы. Например, 4 - VISA, 5 - MasterCard, 2 - МИР. Но согласно ГОСТ ISO/IEC 7812-1—2014, первая цифра должна означать область деятельности:

      1. авиалинии;

      2. авиалинии и другие, возможные в будущем, области деятельности;

      3. путешествия и развлечения и банковское дело/финансовые услуги;

      4. банковское дело/финансовые услуги;

      5. банковское дело/финансовые услуги;

      6. торговля и банковское дело/финансовые услуги;

      7. нефтяная промышленность и другие, возможные в будущем, области деятельности;

      8. здравоохранение, телекоммуникации и другие, возможные в будущем, области деятельности;

      9. для присвоения национальными органами по стандартизации.

      На деле же получаем что банки как бы договариваются друг с другом - авиалинии у себя, а то и не договариваются вовсе, т.к. у каждого своя стойка регистрации и т.д.

      Кроме этого, Вы можете заглянуть в эту папку репозитория и посмотреть какие регулярки применяются для определения принадлежности номера той или иной платёжной системе.


      1. Didimus
        01.07.2023 19:34

        Еще 10-15 лет назад карт МИР не было. А алгоритм был. Потому его тогдашние реализации устарели. Я к чему, алгоритм Луна не относится к определению эмитента карты..


        1. Helldar Автор
          01.07.2023 19:34

          Немного не так. Устарел стандарт, обновлённый в 2014-м году, судя по его полному названию ISO/IEC 7812-1—2014.

          Алгоритм Луна не имеет отношения к определению эмитентов. Он проверяет правильность ввода цифр, не более.

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


  1. AlexeyK77
    01.07.2023 19:34

    У алгоритма Луна есть проблема - он не ловит искажение, если идет перестановка цифр 90<->09


    1. Helldar Автор
      01.07.2023 19:34

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

      Например, числа "12901542" и "12091542" валидны, а вот "12911541" валидно, а "12191541" - нет.