前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >.NET Core 如何生成信用卡卡号

.NET Core 如何生成信用卡卡号

作者头像
Edi Wang
发布2020-03-25 16:33:11
1.4K0
发布2020-03-25 16:33:11
举报
文章被收录于专栏:汪宇杰博客汪宇杰博客

导语

上个月我写了《.NET Core 如何验证信用卡卡号》,不少朋友表示挺有兴趣。在金融科技行业的实际工作中,通常还需要生成信用卡卡号用来测试,今天我就来教大家如何生成信用卡卡号。

上回的改进

上篇文章写完后,我对代码进行了一些改进,除了使用方法上的差别,还改进了一处潜在的性能问题。

原本将卡号字符串转换为int数组的函数为:

public static int[] GetDigitsArrayFromCardNumber(string cardNumber)

{

var digits = cardNumber.Select(p => int.Parse(p.ToString())).ToArray();

return digits;

}

它所存在的问题是,为了将 char 类型转换为 int,做了一次 ToString() 操作,尽管 .NET CLR 会在内存里保留相同内容的 string,但不必要的 string 分配仍会有一定的开销。对于信用卡卡号,此处的 char 一定是代表数字的字符,不可能是其他英文字符或符号,因此可以通过 ASCII 运算来进行高效转换。

我们只需要用 char 减去 '0',即可得到对应的 int 类型,例如 '8' - '0' = 8:

还记得大学计算机基础课里学的 ASCII 码 吗?字符 8 的 ASCII 码为 56,字符 0 的 ASCII 码为 48,因为 56 - 48 = 8,因此字符 8 - 字符 0 = 8。

至于性能的对比,争论再多理论也不如实际测一下有说服力。我们用两种方法,均执行 996007 次(嗯?这个数字有点眼熟),对比总时间。

转换string类型,耗时20ms

使用char计算,耗时 1ms

所以,不要小看这些“骚操作”,平时代码里看到同事这么写不要觉得只是在装逼。尽管有时候代码阅读体验没有那么直观,但如果你的业务面临苛刻的压力时,能够明显体验到性能区别。.NET Core 的基础类库源代码里也有不少类似这样的基础类型骚操作,有兴趣的读者可以去翻翻。

然而装逼,是人类社会的刚需,光用char计算逼格还不够,还记得上回的 Luhn 算法吗?原来的代码如下,我只是把维基百科上公开定义的算法直接翻译成C#:

public static bool IsLuhnValid(int[] digits)

{

var sum = 0;

var alt = false;

for (var i = digits.Length - 1; i >= 0; i--)

{

if (alt)

{

digits[i] *= 2;

if (digits[i] > 9)

{

digits[i] -= 9;

}

}

sum += digits[i];

alt = !alt;

}

return sum % 10 == 0;

}

而C#就应该用出C#的味道不是?代码逼格化以后:

public static bool IsLuhnValid(int[] digits)

{

var sum = digits.Reverse()

.Select((digit, i) =>

(i + 1) % 2 == 0

? digit * 2 > 9 ? digit * 2 - 9 : digit * 2

: digit)

.Sum();

return sum % 10 == 0;

}

深藏功与名。

生成卡号

上回理解了 Luhn 算法之后,我们不难发现,验证卡号的精髓无非在于最后的校验位(Check Digit)。也就是说,生成卡号其实只要生成有效的校验位,其他数字随机,只要校验位正确,就可以通过 Luhn 检查。

校验位生成

还记得校验位怎么来的吗?就拿上回的例子卡号 6011000990139424,去掉校验位4以后,计算的SUM值为4646x9 = 414,尾数为4,即校验位。因此对于我们自己随机生成的卡号,也只要计算除了校验位以外的SUM,然后乘以9,再取尾数即可

因为计算SUM的方法很相似,只是用来翻倍-9的奇偶位不同,所以我重构一下代码,将计算逻辑封装:

public static bool IsLuhnValid(int[] digits)

{

var sum = CalculateSum(digits, 1);

return sum % 10 == 0;

}

private static int CalculateSum(int[] digits, int bitShift = 0)

{

var sum = digits.Reverse()

.Select((digit, i) =>

(i + bitShift) % 2 == 0

? digit * 2 > 9 ? digit * 2 - 9 : digit * 2

: digit)

.Sum();

return sum;

}

生成校验位就能这么操作:

public static int GenerateCheckDigit(int[] digits)

{

var sum = CalculateSum(digits);

var lastDigit = sum * 9 % 10;

return lastDigit;

}

该函数的 digits 参数接受的值是不包含校验位的信用卡其余卡号,例如还是之前的例子 6011000990139424,去掉校验位4,传给 GenerateCheckDigit() 的为 601100099013942。因为少了一位,所以bitShift参数就用默认值0,以确保奇偶位不会错位。

% 10 用来高性能取尾数。嗯?差点又 ToString() 了是吗?

测试计算结果准确,如下:

随机数骚操作

可能大家觉得C#生成随机数有什么难的,不就是一个 Random 类型吗?但实际情况是,如果Random在static修饰符的情况下,这可不一定线程安全,具体原因不在本文讨论范围内,直接给出解决方案。(嗯,差点加锁了是吗?性能可没这个好)

private static int _seed = Environment.TickCount;

private static readonly ThreadLocal<Random> Random =

new ThreadLocal<Random>(() => new Random(Interlocked.Increment(ref _seed)));

参考:https://stackoverflow.com/questions/19270507/correct-way-to-use-random-in-multithread-application

Put Together

实际生成信用卡卡号,一般会给定BIN,因此我的函数设计为接受BIN前缀、卡号位数,生成符合 Luhn 的随机卡号。

public static string GenerateCardNumber(string bin, int length)

{

int[] digits = new int[length];

var prefixDigits = bin.Select(p => p - '0').ToArray();

for (var i = 0; i < prefixDigits.Length; i++)

{

digits[i] = prefixDigits[i];

}

for (var i = bin.Length; i < length - 1; i++)

{

var digit = Random.Value.Next(0, 10);

digits[i] = digit;

}

digits[length - 1] = Luhn.GenerateCheckDigit(digits[..(length -1)]);

return string.Join(null, digits);

}

还是考虑到性能,我没有用 StringBuilder 拼接卡号,更没有用 string += 拼接。设计类库给别人你用的话,一定要注意场景,在我的实际工作中,生成卡号往往是大批量操作,有性能要求,所以写代码要尽量拷问每一处细节。

使用方法:

var bin = "485246";

int length = 16;

var cn = CreditCardGenerator.GenerateCardNumber(bin, length);

Assert.IsNotEmpty(cn);

Assert.IsTrue(cn.Length == length);

var result = CreditCardValidator.ValidCardNumber(cn);

Assert.IsTrue(result.CardNumberFormat == CardNumberFormat.Valid_LuhnOnly

|| result.CardNumberFormat == CardNumberFormat.Valid_BINTest);

批量生成:

var bin = "485246";

int length = 16;

var cardNumbers = new List<string>();

for (int i = 0; i < 128; i++)

{

var cn = CreditCardGenerator.GenerateCardNumber(bin, length);

cardNumbers.Add(cn);

}

var isUnique = cardNumbers.GroupBy(x => x).All(g => g.Count() == 1);

Assert.IsTrue(isUnique);

项目依然在我的交友平台上:https://github.com/EdiWang/Edi.CreditCardUtils

大家如果有什么建议,或是再能进一步改进优化,欢迎提交流及PR。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-03-21,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 汪宇杰博客 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档