
我清楚記得我在本科學習期間第一次接觸CPF(巴西ID)驗證演算法。在申請米納斯吉拉斯聯邦大學 UFMG 精確科學研究所實習時,我們被要求手寫一段 Java 程式碼,在簡單解釋演算法後驗證 CPF 校驗位。
從那時起,我在不同的專業環境中多次遇到這個問題,經常求助於從互聯網複製解決方案並添加一些單元測試。然而,每次,我都會對這些解決方案中反覆出現的問題感到震驚。它們往往更植根於命令式範例,而不是預期的 Java 程式碼物件導向方法。但是,更讓我困擾的是,這些實現帶來的高認知負荷使得閱讀和理解程式碼的意圖變得不切實際。
尚未需要實現此程式碼的有興趣的開發人員可以輕鬆找到任何程式語言的解決方案。然而,它們都傾向於以相同的方式呈現:對 CPF 校驗位如何實現的解釋的簡單複製。似乎很少人花時間去理解這種方法背後的原因。
碰撞問題
在軟體開發中,在雜湊碼演算法中經常會遇到碰撞避免的概念,特別是在使用素數模的情況下。 CPF(巴西ID)和CNPJ(巴西公司ID)中的校驗位功能類似,重點在於避免衝突。這確保了簡單的數字求和不會錯誤地驗證不正確的條目,因為多種組合可以產生相同的總和。
為了緩解這種情況,常見的做法是應用加權和,將每個數字乘以一個特定的因子。您可以將其視為將數字沿著一條線展開;乘法使得多個數字不太可能出現在同一位置。那麼,數字在數字中的位置決定了它的權重,這是有道理的。
為了進一步增強可靠性並最大限度地降低碰撞風險,總和以 11 為模,然後從相同的素數中減去該結果。為了確保校驗位仍然是個位數,10 和 11 的結果將轉換為 0。
認知負荷
用於計算 CPF 和 CNPJ 校驗位的演算法可能很難理解。雖然演算法背後的整體動機可能很清楚,但掌握每個部分的具體作用通常具有挑戰性。出現這種複雜性的部分原因是計算涉及一系列數學計算,這些計算通常集中在單一的大型方法中。此外,通常以莫名其妙的數組形式呈現的權重可能顯得不合邏輯。
為了解決這個問題,我專注於減少缺乏自我解釋的程式碼量。透過堅持單一職責原則(SOLID 中的「S」),我努力創建更簡單、更易於理解的方法。我還努力透過有意義的變數名稱來定義關鍵概念,旨在在程式碼庫中建立一種普遍存在的語言。透過這種方法,我試圖找出用於 CPF 校驗位的方法與用於 CNPJ 的方法的區別,因為需要一種方法的軟體通常需要另一種方法。程式碼的核心功能如下所示,另外,要進一步查看,包括完整的程式碼和相關的單元測試,請造訪我的 GitHub 儲存庫。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | private String getCheckDigits(String document, int maxWeight) {
final int lengthWithoutCheckDigits = getBaseDigitsLength(document);
int firstWeightedSum = 0;
int secondWeightedSum = 0;
for (int i = 0; i < lengthWithoutCheckDigits; i++) {
final int digit = Character.getNumericValue(document.charAt(i));
final int maxIndex = lengthWithoutCheckDigits - 1;
final int reverseIndex = maxIndex - i;
firstWeightedSum += digit * calculateWeight(reverseIndex, maxWeight);
secondWeightedSum += digit * calculateWeight(reverseIndex + 1, maxWeight);
}
final int firstDigit = getCheckDigit(firstWeightedSum);
secondWeightedSum += MIN_WEIGHT * firstDigit;
final int secondDigit = getCheckDigit(secondWeightedSum);
return String.valueOf(firstDigit) + secondDigit;
}
private int calculateWeight(int complementaryIndex, int maxWeight) {
return complementaryIndex % (maxWeight - 1) + MIN_WEIGHT;
}
private int getCheckDigit(int weightedSum) {
final var checkDigit = enhanceCollisionAvoidance(weightedSum);
return checkDigit > 9 ? 0 : checkDigit;
}
private int enhanceCollisionAvoidance(int weightedSum) {
final var weightSumLimit = 11;
return weightSumLimit - weightedSum % weightSumLimit;
}
|
登入後複製
將CNPJ和CPF的校驗位計算結果與網路上找到的典型解決方案進行比較:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 | public class ValidaCNPJ {
public static boolean isCNPJ(String CNPJ) {
if (CNPJ.equals( "00000000000000" ) || CNPJ.equals( "11111111111111" ) ||
CNPJ.equals( "22222222222222" ) || CNPJ.equals( "33333333333333" ) ||
CNPJ.equals( "44444444444444" ) || CNPJ.equals( "55555555555555" ) ||
CNPJ.equals( "66666666666666" ) || CNPJ.equals( "77777777777777" ) ||
CNPJ.equals( "88888888888888" ) || CNPJ.equals( "99999999999999" ) ||
(CNPJ.length() != 14))
return (false);
char dig13, dig14;
int sm, i, r, num, peso;
try {
sm = 0;
peso = 2;
for (i=11; i>=0; i--) {
num = (int)(CNPJ.charAt(i) - 48);
sm = sm + (num * peso);
peso = peso + 1;
if (peso == 10)
peso = 2;
}
r = sm % 11;
if ((r == 0) || (r == 1))
dig13 = '0' ;
else dig13 = (char)((11-r) + 48);
sm = 0;
peso = 2;
for (i=12; i>=0; i--) {
num = (int)(CNPJ.charAt(i)- 48);
sm = sm + (num * peso);
peso = peso + 1;
if (peso == 10)
peso = 2;
}
r = sm % 11;
if ((r == 0) || (r == 1))
dig14 = '0' ;
else dig14 = (char)((11-r) + 48);
if ((dig13 == CNPJ.charAt(12)) && (dig14 == CNPJ.charAt(13)))
return (true);
else return (false);
} catch (InputMismatchException erro) {
return (false);
}
}
}
|
登入後複製
這段程式碼僅供CNPJ使用!
結論
雖然結果程式碼可能顯得有些冗長,但我對清晰度和自我解釋的強調導致了我滿意的結果。程式碼設計得更直觀,對其正確性提供了更大的信心,而且大多數核心功能無需向下滾動頁面即可可見。
我歡迎任何進一步改進的建議,因此請隨時分享您的回饋。
以上是揭秘 CPF 和 CNPJ 校驗位演算法:清晰簡潔的方法的詳細內容。更多資訊請關注PHP中文網其他相關文章!