# Thu, 20 Aug 2026 13:11:52 GMT
---
version: 23
data:


- type: ModificationMethod
  attributes:
    displayName: German tax identification number
    description: |
          This modification method works column based. The input is the value of the current column, which is used to generate a number
          looking like the German tax identification number, which has to fulfill the following requirements:
          * The first number must not be 0
          * In the first 10 digits there is one number double or triple
          * If there are 3 same numbers at the position 1 to 10 those double numbers could never be consecutive
          
          Information about how the check digit is generated and the criteria of a valid number:
          Prüfziffernberechnung (PDF):
          https://www.zfa.deutsche-rentenversicherung-bund.de/de/Inhalt/public/4_ID/47_Pruefziffernberechnung/001_Pruefziffernberechnung.pdf?__blob=publicationFile&v=2
          
          Background information about the number:
          Tax identification number – BZSt:
          https://www.bzst.de/EN/Private_individuals/Tax_identification_number/tax_identification_number_node.html
          
           
    scriptLanguage: "GROOVY"
    legacyScript: false
    inputType: "COLUMN"
    mappingTableContainers: []
    mappingTableAccessMode: READ
    code: |
        /*
        *******************************************************************************
        * The following variables are available in this script:
        * =====================================================
        * ctx             : A context object that offers a number of methods.
        *                   The method indexOf() can be used to get the index of a
        *                   specific column within the data.
        * data            : The data array. This array contains the contents of all
        *                   columns of the current row of the table that is being
        *                   masked by this method.
        *
        * Additionally for column based modification methods the following two
        * variables are available:
        * columnName      : The name of the column that matched the pattern.
        * columnIndex     : The position of the column that matched the pattern within
        *                   the data array. This value is zero-based.
        *******************************************************************************
        */
        
        import de.ubs.xdm3.batch.modification.HashUtils
        
        /**
         * This function is called once before the processing starts. The method
         * is called once per table.
         */
        def init() {
        
        }
        
        /**
         * This function should modify the row that is provided by the data. The
         * method is called for each single row of the table.
         */
        def apply() {
            def seedValue = modificationMethod.seed
        
            def newTaxNumber = []
            while (newTaxNumber.size() < 10) {
                def isValidDigit = false
                def candidateDigit = 0
                def tries = 0
                // Save guard to avoid endless loop
                while (!isValidDigit && tries < 1000) {
                    candidateDigit = HashUtils.hashInt(HashUtils.hashStringLikeInt(seedValue.toString() + newTaxNumber.size() + tries++)) % 10
                    def candidate = newTaxNumber.collect()
                    candidate << candidateDigit
                    isValidDigit = isOccurrencesValid(candidate)
                }
                if (tries == 1000) {
                    throw new RuntimeException("Unable to find valid digit for tax number")
                }
                newTaxNumber << candidateDigit
            }
            data[columnIndex] = newTaxNumber.join('') + getChecksum(newTaxNumber).toString()
            return true
        }
        
        /**
         * This function is executed once after the modification of a table is complete.
         * The method is called once per table.
         */
        def close() {}
        
        /**
         * Check if the digits are valid in order and number of occurrences
         */
        boolean isOccurrencesValid(def taxNumberArray) {
            if (taxNumberArray[0] == 0) {
                return false
            }
            def countCharacters = (0..9).collect { 0 }
            taxNumberArray.each { d -> countCharacters[d as int]++ }
            int num2or3occurrences = 0
            for (int cnt : countCharacters) {
                if (cnt > 3) {
                    return false
                }
                if (cnt >= 2) {
                    num2or3occurrences++
                }
                if (num2or3occurrences > 1) {
                    return false
                }
            }
            for (int i = 0; i < taxNumberArray.size() - 2; i++) {
                if (
                        taxNumberArray[i] == taxNumberArray[i + 1] &&
                                taxNumberArray[i] == taxNumberArray[i + 2]
                ) {
                    return false
                }
            }
            return true
        }
        
        /**
         * Generate the checksum
         */
        int getChecksum(def taxNumberArray) {
            int n = 11
            int m = 10
            int product = m
            int sum
            for (int digit : taxNumberArray) {
                sum = (digit + product) % m
                if (sum == 0) {
                    sum = m
                }
                product = (2 * sum) % n
            }
            int checkDigit = n - product
            return (checkDigit == 10) ? 0 : checkDigit
        }
    parameters:
    - displayName: Seed
      description: |
          The original German tax identification number
      variableName: seed
      dataType: STRING
