Luhn 算法逻辑

Posted

技术标签:

【中文标题】Luhn 算法逻辑【英文标题】:Luhn Algorithm Logic 【发布时间】:2021-05-16 06:37:09 【问题描述】:

我目前正在学习 Codecademy 的全栈工程师课程,到目前为止,我对它非常满意,发现新事物,自己解决问题,但这是我进步的一个严重障碍,我只能这样做'似乎无法识别此逻辑的问题。我并不是要质疑 Luhn 的算法,但我真的需要对此进行澄清……

所以我的问题是,该算法将我的所有数组都返回为有效,我的代码如下(由 codecademy 提供的数组):

// All valid credit card numbers
const valid1 = [4, 5, 3, 9, 6, 7, 7, 9, 0, 8, 0, 1, 6, 8, 0, 8];
const valid2 = [5, 5, 3, 5, 7, 6, 6, 7, 6, 8, 7, 5, 1, 4, 3, 9];
const valid3 = [3, 7, 1, 6, 1, 2, 0, 1, 9, 9, 8, 5, 2, 3, 6];
const valid4 = [6, 0, 1, 1, 1, 4, 4, 3, 4, 0, 6, 8, 2, 9, 0, 5];
const valid5 = [4, 5, 3, 9, 4, 0, 4, 9, 6, 7, 8, 6, 9, 6, 6, 6];

// All invalid credit card numbers
const invalid1 = [4, 5, 3, 2, 7, 7, 8, 7, 7, 1, 0, 9, 1, 7, 9, 5];
const invalid2 = [5, 7, 9, 5, 5, 9, 3, 3, 9, 2, 1, 3, 4, 6, 4, 3];
const invalid3 = [3, 7, 5, 7, 9, 6, 0, 8, 4, 4, 5, 9, 9, 1, 4];
const invalid4 = [6, 0, 1, 1, 1, 2, 7, 9, 6, 1, 7, 7, 7, 9, 3, 5];
const invalid5 = [5, 3, 8, 2, 0, 1, 9, 7, 7, 2, 8, 8, 3, 8, 5, 4];

// Can be either valid or invalid
const mystery1 = [3, 4, 4, 8, 0, 1, 9, 6, 8, 3, 0, 5, 4, 1, 4];
const mystery2 = [5, 4, 6, 6, 1, 0, 0, 8, 6, 1, 6, 2, 0, 2, 3, 9];
const mystery3 = [6, 0, 1, 1, 3, 7, 7, 0, 2, 0, 9, 6, 2, 6, 5, 6, 2, 0, 3];
const mystery4 = [4, 9, 2, 9, 8, 7, 7, 1, 6, 9, 2, 1, 7, 0, 9, 3];
const mystery5 = [4, 9, 1, 3, 5, 4, 0, 4, 6, 3, 0, 7, 2, 5, 2, 3];

// An array of all the arrays above
const batch = [valid1, valid2, valid3, valid4, valid5, invalid1, invalid2, invalid3, invalid4, invalid5, mystery1, mystery2, mystery3, mystery4, mystery5];

以及我实现算法的函数:

const validateCred = arr => 

    let checkSum = 0;
    let ifEvenDouble = 0;
    arr.push(checkSum);

    //Iterate through array, double what is needed

    for(let i = arr.length - 2; i >= 0; i--)
      console.log(ifEvenDouble);

      //If ifEvenDouble is even, we are at the 'other' cell

        if((ifEvenDouble % 2) === 0)
          let doubled = arr[i] * 2;

          //If doubled digit is greater than 9, store sum of individual digits
          //Convert the doubled number to a string then extract each member and convert back to number for calculation, add to checkSum and skip to next iteration, otherwise, add arr[i]

          let newDigit = 0;
          if(doubled > 9)
            newDigit = Number(doubled.toString()[0]) + Number(doubled.toString()[1]);
            //Add doubled & split digit to total and continue the loop
            checkSum += newDigit;
            ifEvenDouble++;
            continue;
          
          //Add doubled digit less than 9 to total and continue the loop
          checkSum += doubled;
          ifEvenDouble++;
          continue;
        

        //Add current array member to total
        checkSum += arr[i];
        ifEvenDouble++;

    //End for loop

    console.log(checkSum);
    const checkDigit = (checkSum * 9) % 10;
    const totalSum = checkDigit + checkSum;

    if(totalSum % 10 === 0)
      console.log('Valid');
      return true;
     else 
      console.log('Invalid');
      return false;
    
;

validateCred(invalid1); // -> Output: Valid

据我了解,我的总和总是将是 10 的倍数,如果我从 10 中减去我的个位,将其添加到我的校验和是总是 给我 10 的倍数。我错了吗?

编辑:我一直在尝试调试它,但我做的越多,离核心算法就越远。

Edit(2):感谢下面的人,我认为我的问题是生成我自己的校验位,而不是使用已经提供的校验位?我的困惑是,通过阅读有关此的***页面,它说:

'计算校验位的例子: 假设一个帐号“7992739871”的示例将添加一个校验位,使其格式为 7992739871x'

然后他们继续用除 x 之外的数字进行所有计算,我认为这是现在主要的困惑。

【问题讨论】:

真正的问题是什么?有什么错误吗?到目前为止,您自己尝试过什么来解决/调试此问题? 我的问题是,根据该算法,本应无效的 cc 号码返回有效。在过去的 3 个小时里,我一直在尝试调试它,但我最终偏离了核心算法,而重点是实现它。 找到一个返回错误结果的循环,单步执行循环并检查出错的位置和时间。 “从 arr.length - 2 开始,它是倒数第二个数组成员” - 这是正确的,因为您必须跳过最右边(最后一个)元素,它位于索引array.length - 1 处,使array.length - 2 处的元素成为第一个使用的元素。但是添加校验和后,您就弄乱了这些数字。 @Andreas 这就是我的想法,但里面有continue。让人很难跟上。正如我的回答所说,对于如此简单的事情,实现起来过于复杂。 【参考方案1】:

我知道问题更多是关于你哪里出错了,而不是更好的解决方案; Jamiec 的回答已经很好地涵盖了这一点。

但是,使用一组数组方法,我们应该能够编写一个更简单的答案。

// utility functions
const sum = (ns) => ns .reduce ((a, b) => a + b, 0)
const last = (xs) => xs [xs .length - 1]

// helper function
const doubleDig = (d) => 2 * d > 9 ? 2 * d - 9 : 2 * d

// main function
const luhn = (ds) =>
  (sum ([... ds] .map (Number) .reverse () .slice(1) .map (
    (d, i) => i % 2 == 0 ? doubleDig (d) : d
  )) + Number (last (ds)))  % 10 == 0

// sample data
const batch = [
  /* Valid   */ [4, 5, 3, 9, 6, 7, 7, 9, 0, 8, 0, 1, 6, 8, 0, 8], [5, 5, 3, 5, 7, 6, 6, 7, 6, 8, 7, 5, 1, 4, 3, 9], [3, 7, 1, 6, 1, 2, 0, 1, 9, 9, 8, 5, 2, 3, 6], [6, 0, 1, 1, 1, 4, 4, 3, 4, 0, 6, 8, 2, 9, 0, 5], [4, 5, 3, 9, 4, 0, 4, 9, 6, 7, 8, 6, 9, 6, 6, 6],
  /* Invalid */ [4, 5, 3, 2, 7, 7, 8, 7, 7, 1, 0, 9, 1, 7, 9, 5], [5, 7, 9, 5, 5, 9, 3, 3, 9, 2, 1, 3, 4, 6, 4, 3], [3, 7, 5, 7, 9, 6, 0, 8, 4, 4, 5, 9, 9, 1, 4], [6, 0, 1, 1, 1, 2, 7, 9, 6, 1, 7, 7, 7, 9, 3, 5], [5, 3, 8, 2, 0, 1, 9, 7, 7, 2, 8, 8, 3, 8, 5, 4],
  /* Mystery */ [3, 4, 4, 8, 0, 1, 9, 6, 8, 3, 0, 5, 4, 1, 4], [5, 4, 6, 6, 1, 0, 0, 8, 6, 1, 6, 2, 0, 2, 3, 9], [6, 0, 1, 1, 3, 7, 7, 0, 2, 0, 9, 6, 2, 6, 5, 6, 2, 0, 3], [4, 9, 2, 9, 8, 7, 7, 1, 6, 9, 2, 1, 7, 0, 9, 3], [4, 9, 1, 3, 5, 4, 0, 4, 6, 3, 0, 7, 2, 5, 2, 3],
]

// demo
console.log (batch .map (luhn))

console .log (
  luhn ('4539677908016808'),
  luhn ('4532778771091795')
)
.as-console-wrapper max-height: 100% !important; top: 0

此函数适用于提供的单个数字数组,但也适用于一串数字,因此luhn ('4539677908016808') 等价于luhn ([4, 5, 3, 9, 6, 7, 7, 9, 0, 8, 0, 1, 6, 8, 0, 8])

首先,我们确保我们使用的是一组数字,[... ds] .map (Number)。然后我们.reverse 数组,以便更容易跟踪偶数和奇数位置,而无需摆弄数组长度。我们.slice 关闭了现在第一个元素,我们稍后只需要它作为校验位。现在我们对结果进行映射,将偶数加倍并根据需要剔除 9(使用辅助函数 doubleDig),但保持奇数不变。我们使用帮助器sum 对结果进行汇总,并使用帮助器last 找到最后一个数字,将其转换为一个数字并将其添加到该总数中。我们通过取模数以 10 为底,并报告该值是否为 0。

那些辅助函数很有用,我几乎总是更喜欢使用这样的函数,但是每个函数都只在我们的主函数中的一个地方调用,这使得如果我们想要内联它们很容易,我们可以写一个支架- 这样做的单独版本:

const luhn = (ds) =>
  ([...ds] .map (Number) .reverse () .slice(1) .map (
    (d, i) => i % 2 == 0 ? (2 * d > 9 ? 2 * d - 9 : 2 * d) : d
  ) .reduce ((a, b) => a + b, 0) + Number (ds [ds .length - 1])) % 10 == 0

我不认为这是一种改进。虽然原始版本很密集,但也不难理解。这个,尤其是当我们将doubleDig 的条件表达式(三元)内联到另一个三元中时,看起来很丑陋。也许更宽敞的布局会有所帮助。1 但是sum (...) 绝对比(...) .reduce ((a , b) => a + b, 0) 更干净,last 比它的替代品更干净。总的来说,这很不鼓舞人心。但最好将其识别为上述辅助功能分解的替代方案。


1 是的,更宽敞的布局改善了这一点很多

const luhn = (ds) =>
  ([...ds]
    .slice (0, -1)
    .reverse () 
    .map (Number) 
    .map ((d, i) => i % 2 == 0 ? (2 * d > 9 ? 2 * d - 9 : 2 * d) : d) 
    .reduce ((a, b) => a + b, 0) 
    + Number (ds [ds .length - 1])) 
    % 10 
    == 0

但更好的是,如果我们更改执行数字加倍例程的奇偶校验,我们不必单独处理校验位。这让我们变得更好

const luhn = ([...ds]) => (ds
  .reverse () 
  .map (Number) 
  .map ((d, i) => i % 2 == 1 ? (2 * d > 9 ? 2 * d - 9 : 2 * d) : d) 
  .reduce ((a, b) => a + b, 0) 
) % 10 == 0

...在这一点上,辅助函数最多是很不错的。这读起来很好。

【讨论】:

谢谢你,scott,老实说,一开始它很令人困惑(你知道什么时候看起来很简单它变得复杂吗?)但就像 Jamiec 的回答一样,这是我可以努力的另一种解决方案能够自己写。非常感谢!【参考方案2】:

您的算法过于复杂。 Wikipedia描述简洁,实现3步即可

    从最右边的数字(不包括校验位)开始向左移动,每隔一个数字加倍。校验位既不加倍也不包含在此计算中;加倍的第一个数字是紧靠校验位左侧的数字。如果这个加倍运算的结果大于 9(例如,8 × 2 = 16),则将结果的数字相加(例如,16:1 + 6 = 7、18:1 + 8 = 9),或者等价地,从结果中减去 9(例如,16:16 - 9 = 7、18:18 - 9 = 9)。 取所有数字的总和(包括校验位)。 如果总计模 10 等于 0(如果总计以 0 结尾)则根据 Luhn 公式该数字有效;否则无效。

我还认为您误解了校验位是什么。您似乎将其作为0 附加到数组中,并尝试在最后计算它。它已经存在于数字中 - 它是最后一个数字。

const validateCred = arr => 

   let doubleIt = true;
   let sum = 0;
   // From the rightmost digit excluding check digit...
   for(let i = arr.length - 2; i >= 0; i--)
        
        if(doubleIt)
          let doubled = arr[i] * 2;
         
          if(doubled > 9)
            doubled -= 9
          
          sum += doubled
        
        else 
          sum += arr[i]
        
        doubleIt = !doubleIt;

    

    // Add the check digit to the sum
    sum += arr[arr.length-1];

    // If sum is divisible by 10 it is valid
    if(sum % 10 === 0)
      console.log('Valid');
      return true;
     else 
      console.log('Invalid');
      return false;
    
;

const invalid1 = [4, 5, 3, 2, 7, 7, 8, 7, 7, 1, 0, 9, 1, 7, 9, 5];
const valid1 = [4, 5, 3, 9, 6, 7, 7, 9, 0, 8, 0, 1, 6, 8, 0, 8];
validateCred(invalid1);
validateCred(valid1);

你出错的地方主要是在校验位的使用上。您似乎正在计算它,而它已经作为数组中的最后一个元素存在。下面的sn-p更接近你原来的,只是没有计算校验位。

const validateCred = arr => 

    let ifEvenDouble = 0;
   let checkSum=0
    //Iterate through array, double what is needed

    for(let i = arr.length - 2; i >= 0; i--)

      //If ifEvenDouble is even, we are at the 'other' cell

        if((ifEvenDouble % 2) === 0)
          let doubled = arr[i] * 2;

          //If doubled digit is greater than 9, store sum of individual digits
          //Convert the doubled number to a string then extract each member and convert back to number for calculation, add to checkSum and skip to next iteration, otherwise, add arr[i]

          let newDigit = 0;
          if(doubled > 9)
            newDigit = Number(doubled.toString()[0]) + Number(doubled.toString()[1]);
            //Add doubled & split digit to total and continue the loop
            checkSum += newDigit;
            ifEvenDouble++;
            continue;
          
          //Add doubled digit less than 9 to total and continue the loop
          checkSum += doubled;
          ifEvenDouble++;
          continue;
        

        //Add current array member to total
        checkSum += arr[i];
        ifEvenDouble++;

    //End for loop

    const checkDigit = arr[arr.length-1]
    const totalSum = checkDigit + checkSum;

    if(totalSum % 10 === 0)
      console.log('Valid');
      return true;
     else 
      console.log('Invalid');
      return false;
    
;


const invalid1 = [4, 5, 3, 2, 7, 7, 8, 7, 7, 1, 0, 9, 1, 7, 9, 5];
const valid1 = [4, 5, 3, 9, 6, 7, 7, 9, 0, 8, 0, 1, 6, 8, 0, 8];
validateCred(invalid1);
validateCred(valid1);

【讨论】:

这看起来很简单,但是我想完全理解这些变化,而不是仅仅为了学习目的而使用它。我有一个问题,因为你没有将校验位推到数组上,1.为什么 for 循环从倒数第二个数字开始,2. arr[arr.length-1] 没有指向最后一个数字在信用卡号?我没有看到任何推送。 “为什么 for 循环从倒数第二个数字开始” - 因为正如我在答案中所说,校验位已经在数组中。因此,length-1 将是校验位,length-2 是第一个需要加倍的数字第一个加倍的数字是位于校验位左侧的数字。 “arr[arr.length-1] 没有指向信用卡号的最后一位数字吗?我没有看到任何推送” - 是的,它指向必须包含在总和中的校验位 Take the所有数字的总和(包括校验位)。 没有任何理由将任何东西推入数组。该数组表示实际的卡号。如果您查看您的信用卡/借记卡,长卡号中的最后一个数字是校验位 我想我现在对此有了更深入的了解,所以我的问题是我认为我必须从卡号生成校验位,但是校验位已经在提供数组? @jkn600 完全正确!只是为了加倍元素而跳过它。更新了与​​您的原始解决方案更接近的解决方案,因此希望您能看到您出错的特定部分。

以上是关于Luhn 算法逻辑的主要内容,如果未能解决你的问题,请参考以下文章

C:信用卡号码检查器/ Luhn 算法

Luhn算法Ruby无法识别AMEX

程序冻结 - Luhn 算法

当我们使用 Luhn 算法验证信用卡时,为啥要反转数字?

增强Luhn算法的实现?

信用卡号码的 Luhn 或 Verhoeff 算法