0
\$\begingroup\$

I want to write a function that validates the German tax identification number (Steuer-ID).
The identification number consists of eleven digits. The last digit is only for checking. This is not what this question is about.

The first ten digits of the identification number contain one digit exactly twice and another digit not at all (as of 2016, a triple occurring digit is also possible, correspondingly then two digits not at all; in case of three equal digits, only two may be in immediate succession, but not all three), the other eight (as of 2016: also seven) digits each exactly once. The first digit must not be 0.

My thought process:

  • With a length of 10 digits, one digit must occur twice anyway. But it may occur three times. So I check if the groupList.Count property is 8 or 9. Probably the Boolean "double_or_triple_occurrence" is unnecessary then...
  • But there must not be a double occurrence of another digit.
  • there must not be 3 of the same digits directly behind each other, only 2 of them.

For Germans among the readers, this is the Wikipedia site.

This is how I implemented it:

Public NotInheritable Class FormMain
    Private Sub FormMain_Load(sender As Object, e As EventArgs) Handles MyBase.Load
        Label1.Text = ""
    End Sub

    Private Sub ButtonStart_Click(sender As Object, e As EventArgs) Handles ButtonStart.Click
        Validate_SteuerId(TextBox1.Text)
    End Sub

    Private Sub Validate_SteuerId(StringToCheck As String)
        Label1.Text = ""
        If String.IsNullOrEmpty(StringToCheck) Then
            Label1.Text = "Leer" ' Empty
            Return
        End If

        If StringToCheck.StartsWith("0") Then
            Label1.Text = "Die erste Ziffer darf nicht 0 sein." ' The first digit must not be zero.
            Return
        End If

        If StringToCheck.Length < 11 Then
            Label1.Text = "Zu kurz" ' too short
            Return
        ElseIf StringToCheck.Length > 11 Then
            Label1.Text = "Zu lang" ' too long
            Return
        End If

        Dim myInts As New List(Of Integer)
        Dim charArray As Char() = StringToCheck.ToArray()
        For i As Integer = 0 To charArray.Length - 2 Step 1
            Dim rslt As Integer
            If Integer.TryParse(charArray(i), rslt) Then
                myInts.Add(rslt)
            Else
                Label1.Text = "Fehler beim Parsen" ' error
                Return
            End If
        Next
        If Not Check_occurrence_of_each_digit(myInts) Then
            Label1.Text = "Fehler"
        End If
    End Sub

    Private Function Check_occurrence_of_each_digit(Ints As List(Of Integer)) As Boolean
        Dim groupList As List(Of IGrouping(Of Integer, Integer)) = Ints.GroupBy(Function(x) x).Where(Function(y) y.Count <= 3).ToList()
        If groupList.Count <> 8 AndAlso groupList.Count <> 9 Then
            Return False
        End If

        Dim double_or_triple_occurrence As Boolean = False

        For i As Integer = 0 To groupList.Count - 1 Step 1
            Dim currentNumber As Integer = groupList(i).Key
            Dim howOften As Integer = groupList(i).Count
            If howOften = 2 OrElse howOften = 3 Then
                double_or_triple_occurrence = True
            End If
            Debug.WriteLine($"Die Zahl {currentNumber} kommt {howOften} Mal vor.")
        Next

        If double_or_triple_occurrence Then
            Dim bools As New List(Of Boolean)
            For i As Integer = 0 To Ints.Count - 1 Step 1
                Dim u As Integer = i
                bools.Add(Ints.Any(Function(x) x = u))
            Next
            If bools.Where(Function(x) x = False).Count > 1 Then
                Return False
            End If
        End If

        'max 2 same digits one after the other
        Dim old As Integer
        Dim middle As Integer
        Dim farest As Integer
        For i As Integer = 0 To Ints.Count - 3 Step 1
            old = Ints(i)
            middle = Ints(i + 1)
            farest = Ints(i + 2)
            If old = Ints(i + 1) AndAlso middle = Ints(i + 1) AndAlso farest = Ints(i + 1) Then
                Return False
            End If
        Next

        Return True
    End Function
End Class

I would like to ask you to shorten the amount of code lines. I appreciate.

\$\endgroup\$
1

1 Answer 1

1
\$\begingroup\$

There is no separation between interface and logic. That also makes it difficult to reuse that function in another context, for instance in a different form. Suggestion: convert your routine into a stand-alone function. All it has to do is to return a result, that can be used by the caller. It should not bother with the UI at all.

Then I assume we will want the function to return two values: one boolean value that tell us whether the check is successful, and another optional value (string) containing a description of the error, if any.

Afaik a VB.net function can return only one value unlike Python for example. To get round this problem you could use a structure or another composite data type. Or you could simply pass additional arguments to your function as ByRef and retrieve their resulting values, after they have been changed by the function.

My proposed implementation is to return a boolean value, and return the error message as a ByRef argument like this:

Private Function Validate_SteuerId(ByVal StringToCheck As String, Optional ByRef ValidationResults As String = "") As Boolean
    If String.IsNullOrEmpty(StringToCheck) Then
        ValidationResults = "Leer" ' Empty
        Return False
    End If

    ' more stuff goes here

    Return True ' default outcome, make sure all code paths return a value

End Function

This is pretty much like you did for Check_occurrence_of_each_digit, we just added an optional return value. Now your function is completely independent from the form. It's easy refactoring. And you can call the function like this when clicking on the button:

Private Sub btnCheck_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnCheck.Click

    Dim reason As String = ""
    If Check_SteuerId(Me.TextBox1.Text, reason) Then
        Label1.Text = "Validation OK"
    Else
        Label1.Text = "Validation failed - reason: " & reason
    End If
End Sub

Alternatives: check if your version of VB.net provides the Tuple or KeyValuePair types.


These two blocks could be merged into one:

If StringToCheck.Length < 11 Then
    Label1.Text = "Zu kurz" ' too short
    Return
ElseIf StringToCheck.Length > 11 Then
    Label1.Text = "Zu lang" ' too long
    Return
End If

Just check if the length is 11, otherwise return an error message saying that 11 characters are expected, that should be enough for the developer. He/she can easily figure out if the input value is too short or too long.


But you can simplify logic a little bit: instead of checking that the length is 11 digits, that the string does not begin with zero etc, you could use a regular expression.

Add this import at the top of your code:

Imports System.Text.RegularExpressions

Then put the block inside your function:

Dim re As New Regex("^[1-9][0-9]{10}$")
If Not re.Match(StringToCheck).Success Then
    ValidationResults = "Expected: a string of 11 digits that does not begin with zero"
    Return False
End If

Using regular expressions and backreferences, it is also possible to verify certain patterns, for example if you wanted to check that the string contains at least one digit repeated exactly 3 times in a row you could do this:

Dim matches As MatchCollection = Regex.Matches(TextBox1.Text, "(.)\1{2}")
Console.WriteLine("matches: " & matches.Count)
' Loop over matches.
For Each m As Match In matches
    ' Loop over captures.
    For Each c As Capture In m.Captures
        ' Display.
        Console.WriteLine("Index={0}, Value={1}", c.Index, c.Value)
    Next
Next

Thus, 12220333456 will return two matches: 222 and 333 respectively. Accordingly matches.Count will be equal to 2.

Now if you want to make sure that no digit is repeated more than twice in a row, you could write Regex.Matches(TextBox1.Text, "(\d)\1{2,}") and verify that matches.Count = 0. Notice the trailing comma after the 2 - it means two or more repetitions of the previously matched digit. 333 will match but 22 won't.

The dot in a regex means any character. \d represents a single digit. Here we are being more precise and match on digits only.

Regular expressions are very powerful but can get very complex too. For some "simple" checks they are convenient shorthand methods and could replace some of your parsing routines.

\$\endgroup\$
1
  • \$\begingroup\$ Hi Kate, thanks for your answer and for the solutions using regex and passing the string by reference. To your point about separating GUI and logic: I have indeed created a new project for you. The logic is now built into the real program. Accepted. 😃 \$\endgroup\$
    – Daniel
    Nov 19, 2022 at 9:15

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.