使用函数式 JavaScript 验证信用卡号
Dolla Dolla Bill,大家好
Dolla Dolla Bill,大家好
信用卡公司每分钟都要处理大量高度敏感的全球网络流量,不容有任何差错。这些公司需要确保不会浪费资源处理不必要的请求。刷信用卡时,处理器必须查找账户以确保其存在,然后查询余额以确保请求的金额可用。虽然单笔交易成本低且金额小,但所涉及的规模却非常巨大。 2016年,仅英国每天
就有3920万笔交易。相关分析预测,到2026年,该地区的交易量将达到6000万笔。显然,任何能够减轻负载的方法都值得探索。
这是一篇入门级的文章。建议读者对 JavaScript 有一定的了解,但不必熟悉函数式编程。
数字代表什么
乍一看,信用卡号似乎只是一串数字。您可能已经注意到,主要的信用卡处理提供商都有自己的前缀。Visa 卡都以 4 开头,MasterCard 以 5 开头,Discover 以 6 开头,American Express 以 3 开头(并且是 15 位数字,而不是 16 位)。此外,金融机构也会有自己的 4-6 位前缀。在销售点系统工作或从事其他金融处理工作的人员很快就会注意到这些模式。例如,Discover 信用卡以 6011 开头,4117 表示美国银行借记卡,5417 表示大通银行。这被称为 BIN,即银行识别号。这里有一个很长的列表。
然而,这完全是网络路由的问题,并且仍然会增加网络负载。为了确保所有查询请求都与真实账户相对应,所有号码都内置了校验和,这是一种检测数据错误的方法。信用卡号由您的发卡机构的 BIN 和您的个人账号组成,但最后一位数字是校验和位,可用于验证错误,而无需查询服务器。
专业提示
“我是BIN和路由号码百科全书”这种开场白可不是个好主意。如果你真的想展现一下自己的实力,先用邮政编码之类的信息缓和一下气氛。观察一下现场气氛。
Luhn算法
这种特定的校验和称为Luhn 公式,美国专利号 2,950,048(但自 1977 年起属于公共领域)。要通过 Luhn 算法验证数字,需要添加一个校验位。然后,对原始数字执行该公式后,查看该校验位是否与结果相符。
-
将整个数字拆分成单独的数字。
-
从最右边开始(不包括校验位)并每秒翻一番,向左移动。
-
如果任何一位两位数大于 9,则将这些数字相加(或者减去 9,如果你愿意的话)。
-
将所有数字与校验位相加。
-
如果总数模 10 等于 0,则该数字有效。
例如,该号码4012-8888-8888-1881
是一个有效的 Visa 格式的账号,用于测试。您无法对其进行扣款,但它应该可以通过此算法进行验证。
-
拆分成数字:
4 0 1 2 8 8 8 8 8 8 8 8 1 8 8 1
。 -
除校验位外,每秒翻一番,从右到左:
8 0 2 2 16 8 16 8 16 8 16 8 2 8 16 1
。 -
添加任意九以上的数字:
8 0 2 2 7 8 7 8 7 8 7 8 2 8 7 1
。 -
将数字相加:
90
。 -
它是 10 的倍数吗?是的!
经过检查,这个号码可能是一张有效的 Visa 卡,因此我们可以发出网络请求。
实施
要继续学习,你需要Node。我使用的是pnpm,你也可以使用npm
或yarn
。创建一个新项目:
$ mkdir luhn
$ cd luhn
$ pnpm init
// follow prompts
$ touch index.js
把存根扔进去就index.js
可以连接起来:
const luhn = {};
luhn.validate = numString => {
return false;
};
module.exports = luhn;
单元测试
在开始实施之前,最好先准备好一些单元测试。添加mocha
:
$ pnpm install mocha
$ mkdir test
$ touch test/test.js
在 中package.json
,设置test
要运行的脚本mocha
:
"scripts": {
"test": "mocha"
},
现在添加以下测试test/test.js
:
const assert = require("assert").strict;
const luhn = require("../index.js");
describe("luhn", function() {
describe("#validate()", function() {
it("should accept valid Visa test number", function() {
assert.ok(luhn.validate("4012-8888-8888-1881"));
});
it("should accept valid MasterCard test number", function() {
assert.ok(luhn.validate("5105-1051-0510-5100"));
});
it("should accept valid Amex test number", function() {
assert.ok(luhn.validate("3714-496353-98431"));
});
it("should reject invalid numbers", function() {
assert.equal(luhn.validate("1234-5678-9101-2131"), false);
});
});
});
别担心,这些不是真实账户,只是一些来自这里的有效测试号码。
正如预期的那样,运行npm test
应该确认我们的存根有一些工作要做:
Luhn
#validate()
1) should accept valid Visa test number
2) should accept valid MasterCard test number
3) should accept valid Amex test number
✓ should reject invalid numbers
我坚持采用函数式风格来实现此目的,其中我们不是改变状态和循环,而是通过定义数据转换来获得最终结果。
拆分数字
第一步是从传入的字符串中取出数字。我们可以使用 来丢弃所有非数字部分String.prototype.replace()
。
const to_digits = numString =>
numString
.replace(/[^0-9]/g, "")
.split("")
.map(Number);
正则表达式用于^
匹配除 0-9之外的任何数字。尾部g
表示我们要进行全局匹配,并将所有匹配结果替换为空(即从字符串中删除)。如果省略,则仅替换第一个匹配项,其余字符串保持不变。然后,我们将每个数字拆分成单个字符,并将它们全部从字符转换为数值。
设置舞台
回到luhn.validate()
,让我们使用此函数存储我们的数字数组并保留校验位以供日后使用:
luhn.validate = numString => {
+ const digits = to_digits(numString);
+ const len = digits.length;
+ const luhn_digit = digits[len - 1];
+ const total = 0; // TODO
return false;
};
为了进行最终验证,我们将对这个数字数组进行一系列转换,以将其缩减为最终总数。有效数字将产生 10 的倍数的结果:
luhn.validate = numString => {
const digits = to_digits(numString);
const len = digits.length;
const luhn_digit = digits[len - 1];
const total = 0; // TODO
- return false;
+ return total % 10 === 0;
};
获取总计
我们已经用英语讲过了。我们来用伪代码演示一下:
const total = digits
.doubleEveryOtherFromRightMinusCheckDigit()
.map(reduceMultiDigitVals)
.addAllDigits();
我们必须对帐号中的正确数字执行加倍步骤,然后转换最终为多位数字的任何内容,然后将所有数字加在一起。
对于此步骤,我们可以使用Array.prototype.slice()
获取数字数组的子集,该子集包含除校验位之外的所有内容。从右到左的顺序可以通过以下方式实现Array.prototype.reverse()
:
const total = digits
- .doubleveryOtherFromRightMinusCheckDigit()
+ .slice(0, -1)
+ .reverse()
+ .map(doubleEveryOther)
.map(reduceMultiDigitVals)
.addAllDigits();
这些Array.prototype.map()
调用可以保持原样,我们稍后可以定义所需的函数。最后一步,将所有元素加在一起,可以用 来处理Array.prototype.reduce()
。此方法通过对每个元素和累加器调用一个函数,从集合中生成单个结果。通过将每个元素添加到累计总数中,我们可以得出一个和。不过,我们不是从 0 开始,而是从之前存储的校验和数字开始:
const total = digits
.slice(0, -1)
.reverse()
.map(doubleEveryOther)
.map(reduceMultiDigitVals)
- .addAllDigits()
+ .reduce((current, accumulator) => current + accumulator, luhn_digit);
一切顺利!
定义转换
上面的管道中还有两个未定义的操作:doubleEveryOther
和reduceMultiDigitVals
。在这两个操作中,我们都会遍历每个数字,并根据条件调整其值。要么是每隔一个数字调整一次,要么是当某个数字大于某个阈值时调整一次。但这两种情况下,基本映射函数都采用相同的格式——它会根据条件进行转换:
const condTransform = (predicate, value, fn) => {
if (predicate) {
return fn(value);
} else {
return value;
}
};
这有点像三元运算符,但它是一个函数。它的每个实例都只是条件变换的一个特定情况:
const doubleEveryOther = (current, idx) =>
condTransform(idx % 2 === 0, current, x => x * 2);
const reduceMultiDigitVals = current =>
condTransform(current > 9, current, x => x - 9);
这两个函数都接受与 兼容的参数列表map()
,因此可以直接插入。其中一个包含当前元素的索引,另一个不包含,两者都会直接传递给这个辅助转换函数。如果满足谓词,元素将根据最终的转换函数进行转换,否则保持不变。
总结
总结一下:
const to_digits = numString =>
numString
.replace(/[^0-9]/g, "")
.split("")
.map(Number);
const condTransform = (predicate, value, fn) => {
if (predicate) {
return fn(value);
} else {
return value;
}
};
const doubleEveryOther = (current, idx) =>
condTransform(idx % 2 === 0, current, x => x * 2);
const reduceMultiDigitVals = current =>
condTransform(current > 9, current, x => x - 9);
const luhn = {};
luhn.validate = numString => {
const digits = to_digits(numString);
const len = digits.length;
const luhn_digit = digits[len - 1];
const total = digits
.slice(0, -1)
.reverse()
.map(doubleEveryOther)
.map(reduceMultiDigitVals)
.reduce((current, accumulator) => current + accumulator, luhn_digit);
return total % 10 === 0;
};
module.exports = luhn;
检查一下pnpm test
:
luhn
#validate()
✓ should accept valid Visa test number
✓ should accept valid MasterCard test number
✓ should accept valid Amex test number
✓ should reject invalid numbers
4 passing (3ms)
该算法可用于各种不同类型的数据验证,而不仅仅是信用卡号。也许您可以将其集成到下一个项目的设计中!在数据库密钥中添加校验和有助于防止数据传输错误,而且像这样的简单验证很容易上手。
挑战
扩展此代码,提供一种方法,将正确的 Luhn 校验和添加到任意数字中。校验位就是您需要添加到总数中,以使其达到 10 的倍数的数字。
照片由 Clay Banks 在 Unsplash 上拍摄
文章来源:https://dev.to/decidously/validate-a-credit-card-number-with-function-javascript-54oe