首页  >  文章  >  Java  >  揭秘 CPF 和 CNPJ 校验位算法:清晰简洁的方法

揭秘 CPF 和 CNPJ 校验位算法:清晰简洁的方法

PHPz
PHPz原创
2024-09-03 12:32:03613浏览

Demystifying CPF and CNPJ Check Digit Algorithms: A Clear and Concise Approach

我清楚地记得我在本科学习期间第一次接触CPF(巴西ID)验证算法。在申请米纳斯吉拉斯州联邦大学 UFMG 精确科学研究所实习时,我们被要求手工编写一段 Java 代码,在简要解释算法后验证 CPF 校验位。

从那时起,我在不同的专业环境中多次遇到这个问题,经常求助于从互联网复制解决方案并添加一些单元测试。然而,每次,我都会对这些解决方案中反复出现的问题感到震惊。它们往往更植根于命令式范例,而不是预期的 Java 代码面向对象方法。但是,更让我困扰的是,这些实现带来的高认知负荷使得阅读和理解代码的意图变得不切实际。

尚未需要实现此代码的感兴趣的开发人员可以轻松找到任何编程语言的解决方案。然而,它们都倾向于以相同的方式呈现:对 CPF 校验位如何实现的解释的简单复制。似乎很少有人花时间去理解这种方法背后的原因。

碰撞问题

在软件开发中,哈希码算法中经常会遇到碰撞避免的概念,特别是在使用素数模的情况下。 CPF(巴西ID)和CNPJ(巴西公司ID)中的校验位功能类似,重点是避免冲突。这确保了简单的数字求和不会错误地验证不正确的条目,因为多种组合可以产生相同的总和。

为了缓解这种情况,常见的做法是应用加权和,将每个数字乘以一个特定的因子。您可以将其视为将数字沿一条线展开;乘法使得多个数字不太可能出现在同一位置。那么,数字在数字中的位置决定了它的权重,这是有道理的。

为了进一步增强可靠性并最大限度地降低碰撞风险,总和以 11 为模,然后从相同的素数中减去该结果。为了确保校验位仍然是个位数,10 和 11 的结果将转换为 0。

认知负荷

用于计算 CPF 和 CNPJ 校验位的算法可能很难理解。虽然算法背后的总体动机可能很清楚,但掌握每个部分的具体作用通常具有挑战性。出现这种复杂性的部分原因是计算涉及一系列数学计算,这些数学计算通常集中在一个单一的大型方法中。此外,通常以莫名其妙的数组形式呈现的权重可能显得不合逻辑。

为了解决这个问题,我专注于减少缺乏自我解释的代码量。通过坚持单一职责原则(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中文网其他相关文章!

声明:
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn