Kiểm tra tính hợp lệ của số thẻ tín dụng sử dụng thuật toán Luhn (Mod 10) - tiêu chuẩn quốc tế ISO/IEC 7812-1.
Quy tắc cơ bản
- Độ dài: 13-16 chữ số.
- Prefix (số bắt đầu):
- Visa:
4 - MasterCard:
51-55hoặc2221-2720(mới) - American Express:
34,37 - Discover:
6011,65,644-649
- Visa:
- Checksum Luhn: Tổng các chữ số sau xử lý phải chia hết cho 10.
Thuật toán Luhn (Mod 10)
- Từ phải sang trái, nhân đôi các chữ số ở vị trí chẵn (tính từ 0: vị trí 1, 3, 5... từ phải).
- Nếu nhân đôi ≥ 10, cộng 2 chữ số (ví dụ: 8×2=16 → 1+6=7). Hoặc trừ 9.
- Cộng tất cả chữ số ở vị trí lẻ (từ phải) + kết quả bước 2.
- Nếu tổng % 10 == 0 → Hợp lệ.
Ví dụ hoàn chỉnh (Sửa lỗi logic code gốc)
public class CreditCardValidator {
public static void main(String[] args) {
String[] testCards = {
"4440967484181607", // Visa valid
"379354508162306", // Amex valid
"5555555555554444", // MasterCard valid
"6011111111111117", // Discover valid
"1234567890123456", // Invalid (Luhn fail)
"4440967484181608", // Invalid (checksum fail)
};
for (String card : testCards) {
boolean valid = isValid(card);
System.out.printf("%s -> %s%n", card, valid ? "VALID" : "INVALID");
}
}
public static boolean isValid(String cardNumber) {
// 1. Chỉ chứa chữ số, độ dài 13-19
if (cardNumber == null || !cardNumber.matches("\\d{13,19}")) {
return false;
}
// 2. Kiểm tra prefix (Issuer Identification Number)
if (!hasValidPrefix(cardNumber)) {
return false;
}
// 3. Luhn checksum
return luhnCheck(cardNumber);
}
private static boolean hasValidPrefix(String card) {
// Visa: 4
if (card.startsWith("4")) return true;
// MasterCard: 51-55, 2221-2720
if (card.startsWith("5")) {
int prefix2 = Integer.parseInt(card.substring(0, 2));
if (prefix2 >= 51 && prefix2 <= 55) return true;
if (card.length() >= 4) {
int prefix4 = Integer.parseInt(card.substring(0, 4));
if (prefix4 >= 2221 && prefix4 <= 2720) return true;
}
}
// Amex: 34, 37
if (card.startsWith("34") || card.startsWith("37")) return true;
// Discover: 6011, 65, 644-649
if (card.startsWith("6011")) return true;
if (card.startsWith("65")) return true;
if (card.length() >= 3) {
int prefix3 = Integer.parseInt(card.substring(0, 3));
if (prefix3 >= 644 && prefix3 <= 649) return true;
}
return false;
}
private static boolean luhnCheck(String card) {
int sum = 0;
boolean alternate = false; // Bắt đầu từ phải: vị trí 0 (lẻ) -> không nhân đôi
for (int i = card.length() - 1; i >= 0; i--) {
int digit = card.charAt(i) - '0';
if (alternate) {
digit *= 2;
if (digit > 9) digit -= 9; // Tương đương cộng 2 chữ số
}
sum += digit;
alternate = !alternate;
}
return sum % 10 == 0;
}
}
Kết quả
4440967484181607 -> VALID 379354508162306 -> VALID 5555555555554444 -> VALID 6011111111111117 -> VALID 1234567890123456 -> INVALID 4440967484181608 -> INVALID
Phân tích lỗi code gốc
- Code gốc dùng
longlàm tham số → tràn số cho thẻ 16 chữ số (Long.MAX_VALUE ≈ 9.22e18, thẻ 16 số max 9.99e15 - vừa nhưng rủi ro). - Hàm
thesize(long)chuyển long → String → length() kém hiệu quả. - Logic
prefixmatchvàgetprefxphức tạp hóa đơn giản. - Không validate prefix MasterCard/Amex/Discover đầy đủ.
Best Practices
- Nhận input là
String(giữ leading zeros, không tràn, dễ validate regex). - Dùng
charAt() - '0'thay vìInteger.parseInt(char + ""). - Tách riêng logic: validate format → validate prefix → Luhn check.
- Trả về
enumhoặc object chi tiết (valid, cardType, errorReason) thay vì boolean.
Mở rộng: Xác định loại thẻ
public enum CardType {
VISA, MASTERCARD, AMEX, DISCOVER, UNKNOWN
}
public static CardType detectType(String card) {
if (card == null) return CardType.UNKNOWN;
if (card.matches("^4\\d{12,18}$")) return CardType.VISA;
if (card.matches("^(5[1-5]\\d{14}|2(2[2-9]\\d|[3-6]\\d{2}|7[0-1]\\d|720)\\d{12})$")) return CardType.MASTERCARD;
if (card.matches("^3[47]\\d{13}$")) return CardType.AMEX;
if (card.matches("^(6011\\d{12}|65\\d{14}|64[4-9]\\d{13})$")) return CardType.DISCOVER;
return CardType.UNKNOWN;
}