私は学部時代に CPF (ブラジル ID) 検証アルゴリズムに初めて遭遇したことを鮮明に覚えています。ミナスジェライス連邦大学 UFMG 精密科学研究所でのインターンシップに応募する際、アルゴリズムの簡単な説明の後、CPF チェック ディジットを検証する Java コードを手動で書くように求められました。
それ以来、私はさまざまな専門的な状況でこの問題に何度か遭遇し、多くの場合、インターネットからソリューションをコピーして単体テストを追加することに頼っていました。しかし、そのたびに、これらのソリューションで繰り返し発生する問題に驚かされます。これらは、Java コードに期待されるオブジェクト指向のアプローチよりも、命令型パラダイムに根ざしている傾向があります。しかし、さらに私を悩ませているのは、これらの実装が課す高い認知負荷により、コードを読んで意図を理解することが非現実的になってしまうことです。
まだこのコードを実装する必要がない興味のある開発者は、任意のプログラミング言語でソリューションを簡単に見つけることができます。ただし、それらはすべて同じ方法で提示される傾向があります。つまり、CPF チェック ディジットがどのように実装されるかについての説明の単純な複製です。このアプローチの背後にある理由を時間をかけて理解する人はほとんどいないようです。
ソフトウェア開発では、衝突回避の概念がハッシュ コード アルゴリズム、特に素数法を使用する際に頻繁に登場します。 CPF (ブラジル ID) と CNPJ (ブラジル企業 ID) のチェック ディジットは、衝突の回避に重点を置いて同様に機能します。これにより、複数の組み合わせが同じ合計を生成する可能性があるため、単純な数字の合計が間違ったエントリを誤って検証することがなくなります。
これを軽減するために、各桁に特定の係数を掛けて加重合計を適用するのが一般的です。これは、数字を線に沿って広げていると考えることができます。乗算により、複数の数字が同じ位置に配置される可能性が低くなります。したがって、数値内の桁の位置によってその重みが決まることは理にかなっています。
信頼性をさらに高め、衝突のリスクを最小限に抑えるために、合計は 11 を法にして計算され、この結果が同じ素数から減算されます。チェック デジットが 1 桁のままであることを保証するために、10 と 11 の結果は 0 に変換されます。
CPF と CNPJ のチェック ディジットの計算に使用されるアルゴリズムは、理解するのが難しい場合があります。アルゴリズムの背後にある全体的な動機は明らかかもしれませんが、各部分の具体的な役割を把握するのは難しいことがよくあります。この複雑さの原因の 1 つは、計算には一連の数学的計算が含まれ、それらの計算が 1 つの大規模なメソッドにまとめられることが多いためです。さらに、通常、重みは説明できない配列として表示され、非論理的に見える可能性があります。
これに対処するために、私は説明を省略したコードの量を減らすことに重点を置いています。単一責任原則 (SOLID の「S」) に従うことで、よりシンプルでわかりやすいメソッドを作成するよう努めています。また、コードベース内でユビキタス言語を確立することを目指して、意味のある変数名を通じて主要な概念を定義するよう努めています。このアプローチでは、一方を必要とするソフトウェアにはもう一方も必要になることが多いため、CPF チェック ディジットに使用される方法と CNPJ に使用される方法の違いを特定しようとしました。コードのコア機能を以下に示します。また、完全なコードと関連する単体テストを含む詳細については、私の GitHub リポジトリにアクセスしてください。
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); // Index is incremented, starting from 3, skipping first check digit. // The first part will be added later as the calculated first check digit times its corresponding weight. secondWeightedSum += digit * calculateWeight(reverseIndex + 1, maxWeight); } final int firstDigit = getCheckDigit(firstWeightedSum); // Add the first part as the first check digit times the first weight. 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 の両方のチェック ディジットを計算した結果を、インターネットで見つかった典型的な解決策と比較します。
public class ValidaCNPJ { public static boolean isCNPJ(String CNPJ) { // considera-se erro CNPJ's formados por uma sequencia de numeros iguais 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" - protege o código para eventuais erros de conversao de tipo (int) try { // Calculo do 1o. Digito Verificador sm = 0; peso = 2; for (i=11; i>=0; i--) { // converte o i-ésimo caractere do CNPJ em um número: // por exemplo, transforma o caractere '0' no inteiro 0 // (48 eh a posição de '0' na tabela ASCII) 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); // Calculo do 2o. Digito Verificador 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); // Verifica se os dígitos calculados conferem com os dígitos informados. if ((dig13 == CNPJ.charAt(12)) && (dig14 == CNPJ.charAt(13))) return(true); else return(false); } catch (InputMismatchException erro) { return(false); } } }
このコードは CNPJ 専用です!
結果のコードはやや冗長に見えるかもしれませんが、明確さと説明を重視した結果、満足のいく結果が得られました。コードはより直観的になるように設計されており、コードの正確さに対する信頼性が高く、また、コア機能のほとんどはページを下にスクロールしなくても表示されます。
さらなる改善のための提案を歓迎しますので、お気軽にフィードバックを共有してください。
以上がCPF および CNPJ チェック デジット アルゴリズムの謎を解く: 明確かつ簡潔なアプローチの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。