1
\$\begingroup\$

I've found several examples of code to increment various sequence values but they are usually specific to either alpha characters or numeric characters. I wanted something that could increment a sequence to append as a suffix to a file name to "ensure" uniqueness. The "catch" is that sometimes I want numeric values and other times I want alpha characters, while in some cases I might want alphanumeric or even special characters in that file name suffix. I also want to be able to specify a "default length" for the suffix (e.g., I might want the suffix to be 0094 instead of just 94.)

After several revisions, I've written the following which appears to be generating the results I want/expect:

''' <summary>
''' Defines a specific set of characters to find, remove, or replace in an existing string
''' </summary>
Public Enum CharacterType
    ''' <summary>
    ''' No character type specified
    ''' </summary>
    NONE

    ''' <summary>
    ''' All printable characters
    ''' </summary>
    All

    ''' <summary>
    ''' Alpha characters (A-Z or a-z) and/or numeric characters (0-9)
    ''' </summary>
    AlphaNumeric

    ''' <summary>
    ''' Alpha characters (A-Z or a-z)
    ''' </summary>
    Alpha

    ''' <summary>
    ''' Numeric characters (0-9)
    ''' </summary>
    Numeric

    ''' <summary>
    ''' Punctuation or special characters (e.g., ?, #, $, @, !, etc.)
    ''' </summary>
    Punctuation
End Enum

''' <summary>
''' Increments a sequence value to the next acceptable value for the specified <paramref name="SequenceStyle"/>
''' </summary>
''' <param name="CurrentValue">The current value of the sequence to be incremented.</param>
''' <param name="SequenceStyle">The type(s) of characters that are valid in the sequence return value.</param>
''' <param name="Length">The default length of the sequence. The current value will be padded if its length is less than this value.</param>
''' <returns>
''' A <see cref="String"/> value of the incremented sequence.
''' </returns>
Public Shared Function IncrementSequence(ByVal CurrentValue As String,
                                         Optional ByVal SequenceStyle As CharacterType = CharacterType.NONE,
                                         Optional ByVal Length As Integer = 1,
                                         Optional ByVal ThrowOnInvalidChars As Boolean = False) As String
    Dim SkipCharBuffer As String = " !""#$%&'()*+,-./:;<=>?@[\]^_`{|}~"
    Dim SkipChars As Char() = SkipCharBuffer.ToCharArray

    Select Case SequenceStyle
        Case CharacterType.Alpha
            If CurrentValue Is Nothing OrElse String.IsNullOrEmpty(CurrentValue.Trim) Then
                Return "A".PadLeft(Length, "A"c)
            Else
                Dim SequenceChars As Char() = CurrentValue.PadLeft(Length, "A"c).ToUpper.ToCharArray
                Dim I As Integer = SequenceChars.Length - 1
                Dim Alpha As Integer = Asc(SequenceChars(I))

                Do While I >= 0
                    If SkipChars.Contains(SequenceChars(I)) Then
                        If ThrowOnInvalidChars Then
                            Throw New FormatException("Invalid characters found in the current sequence value.")
                        Else
                            I -= 1
                        End If
                    ElseIf Alpha >= 65 AndAlso Alpha < 90 Then
                        SequenceChars(I) = Chr(Asc(SequenceChars(I)) + 1)
                        Return New String(SequenceChars)
                    ElseIf Alpha >= 90 Then
                        SequenceChars(I) = Chr(65)
                        I -= 1

                        If I < 0 Then
                            Dim TempArray(SequenceChars.Length) As Char

                            For X As Integer = 1 To SequenceChars.Length
                                TempArray(X) = SequenceChars(X - 1)
                            Next X

                            TempArray(0) = Chr(65)
                            SequenceChars = TempArray
                            Return New String(SequenceChars)
                        Else
                            Alpha = Asc(SequenceChars(I))
                        End If
                    Else
                        If ThrowOnInvalidChars Then
                            Throw New FormatException("Invalid characters found in the current sequence value.")
                        End If
                    End If
                Loop

                Return New String(SequenceChars)
            End If
        Case CharacterType.Numeric
            If CurrentValue Is Nothing OrElse String.IsNullOrEmpty(CurrentValue.Trim) Then
                Return "1".PadLeft(Length, "0"c)
            Else
                Dim SequenceChars As Char() = CurrentValue.PadLeft(Length, "0"c).ToUpper.ToCharArray
                Dim I As Integer = SequenceChars.Length - 1
                Dim Alpha As Integer = Asc(SequenceChars(I))

                Do While I >= 0
                    If SkipChars.Contains(SequenceChars(I)) Then
                        If ThrowOnInvalidChars Then
                            Throw New FormatException("Invalid characters found in the current sequence value.")
                        Else
                            I -= 1
                        End If
                    ElseIf Alpha < 48 Then
                        SequenceChars(I) = Chr(48)
                        Return New String(SequenceChars)
                    ElseIf Alpha >= 48 AndAlso Alpha < 57 Then
                        SequenceChars(I) = Chr(Asc(SequenceChars(I)) + 1)
                        Return New String(SequenceChars)
                    ElseIf Alpha >= 57 Then
                        SequenceChars(I) = Chr(48)
                        I -= 1

                        If I < 0 Then
                            Dim TempArray(SequenceChars.Length) As Char

                            For X As Integer = 1 To SequenceChars.Length
                                TempArray(X) = SequenceChars(X - 1)
                            Next X

                            TempArray(0) = Chr(49)
                            SequenceChars = TempArray
                            Return New String(SequenceChars)
                        Else
                            Alpha = Asc(SequenceChars(I))
                        End If
                    Else
                        If ThrowOnInvalidChars Then
                            Throw New FormatException("Invalid characters found in the current sequence value.")
                        End If
                    End If
                Loop

                Return New String(SequenceChars)
            End If
        Case CharacterType.AlphaNumeric
            If CurrentValue Is Nothing OrElse String.IsNullOrEmpty(CurrentValue.Trim) Then
                Return "1".PadLeft(Length, "0"c)
            Else
                Dim SequenceChars As Char() = CurrentValue.ToUpper.PadLeft(Length, "0"c).ToCharArray
                Dim I As Integer = SequenceChars.Length - 1
                Dim Alpha As Integer = Asc(SequenceChars(I))

                Do While I >= 0
                    If SkipChars.Contains(SequenceChars(I)) Then
                        If ThrowOnInvalidChars Then
                            Throw New FormatException("Invalid characters found in the current sequence value.")
                        Else
                            I -= 1
                        End If
                    ElseIf Alpha < 48 Then
                        SequenceChars(I) = Chr(48)
                        Return New String(SequenceChars)
                    ElseIf Alpha >= 48 AndAlso Alpha < 57 Then
                        SequenceChars(I) = Chr(Asc(SequenceChars(I)) + 1)
                        Return New String(SequenceChars)
                    ElseIf Alpha >= 57 AndAlso Alpha < 65 Then
                        SequenceChars(I) = Chr(65)
                        Return New String(SequenceChars)
                    ElseIf Alpha >= 65 AndAlso Alpha < 90 Then
                        SequenceChars(I) = Chr(Asc(SequenceChars(I)) + 1)
                        Return New String(SequenceChars)
                    ElseIf Alpha >= 90 Then
                        SequenceChars(I) = Chr(48)
                        I -= 1

                        If I < 0 Then
                            Dim TempArray(SequenceChars.Length) As Char

                            For X As Integer = 1 To SequenceChars.Length
                                TempArray(X) = SequenceChars(X - 1)
                            Next X

                            TempArray(0) = Chr(49)
                            SequenceChars = TempArray
                            Return New String(SequenceChars)
                        Else
                            Alpha = Asc(SequenceChars(I))
                        End If
                    Else
                        If ThrowOnInvalidChars Then
                            Throw New FormatException("Invalid characters found in the current sequence value.")
                        End If
                    End If
                Loop

                Return New String(SequenceChars)
            End If
        Case CharacterType.All
            If CurrentValue Is Nothing OrElse String.IsNullOrEmpty(CurrentValue.Trim) Then
                Return "1".PadLeft(Length, "0"c)
            Else
                Dim SequenceChars As Char() = CurrentValue.ToUpper.PadLeft(Length, "0"c).ToCharArray
                Dim I As Integer
                Dim Alpha As Integer

                For C As Integer = 0 To SequenceChars.Length - 1
                    If Path.GetInvalidFileNameChars.Contains(SequenceChars(C)) OrElse Path.GetInvalidPathChars.Contains(SequenceChars(C)) Then
                        SequenceChars(C) = "_"c
                    ElseIf Asc(SequenceChars(C)) >= 0 AndAlso Asc(SequenceChars(C)) <= 31 Then
                        SequenceChars(C) = "_"c
                    ElseIf Asc(SequenceChars(C)) = 124 Then
                        SequenceChars(C) = "_"c
                    ElseIf Asc(SequenceChars(C)) = 127 Then
                        SequenceChars(C) = "_"c
                    End If
                Next C

                I = SequenceChars.Length - 1

                Do While I >= 0
                    Alpha = Asc(SequenceChars(I))

                    If SkipChars.Contains(SequenceChars(I)) Then
                        I -= 1
                    ElseIf Alpha < 48 Then
                        SequenceChars(I) = Chr(48)
                        Return New String(SequenceChars)
                    ElseIf Alpha >= 48 AndAlso Alpha < 57 Then
                        SequenceChars(I) = Chr(Asc(SequenceChars(I)) + 1)
                        Return New String(SequenceChars)
                    ElseIf Alpha >= 57 AndAlso Alpha < 65 Then
                        SequenceChars(I) = Chr(65)
                        Return New String(SequenceChars)
                    ElseIf Alpha >= 65 AndAlso Alpha < 90 Then
                        SequenceChars(I) = Chr(Asc(SequenceChars(I)) + 1)
                        Return New String(SequenceChars)
                    ElseIf Alpha >= 90 Then
                        SequenceChars(I) = Chr(48)
                        I -= 1

                        If I < 0 Then
                            Dim TempArray(SequenceChars.Length) As Char

                            For X As Integer = 1 To SequenceChars.Length
                                TempArray(X) = SequenceChars(X - 1)
                            Next X

                            TempArray(0) = Chr(49)
                            SequenceChars = TempArray
                            Return New String(SequenceChars)
                        End If
                    End If
                Loop

                Return New String(SequenceChars)
            End If
        Case CharacterType.NONE
            Return IncrementSequence(CurrentValue, GetSequenceStyle(CurrentValue))
        Case Else
            If ThrowOnInvalidChars Then
                Throw New ArgumentOutOfRangeException(NameOf(SequenceStyle), SequenceStyle, $"The specified {NameOf(CharacterType)} does not exist.")
            Else
                Return Nothing
            End If
    End Select
End Function

''' <summary>
''' Gets the type(s) of characters that exist in the current sequence value.
''' </summary>
''' <param name="CurrentSequenceValue">The sequence to check for character type(s).</param>
''' <returns>
''' The <see cref="CharacterType"/> value identifying the valid type(s) of characters 
''' for the specified <paramref name="CurrentSequenceValue"/>
''' </returns>
Private Shared Function GetSequenceStyle(ByVal CurrentSequenceValue As String) As CharacterType
    Dim Alpha As Boolean = False
    Dim Numeric As Boolean = False
    Dim Punctuation As Boolean = False

    For Each C As Char In CurrentSequenceValue.ToUpper.ToCharArray
        If Char.IsNumber(C) Then
            Numeric = True
        ElseIf Char.IsLetter(C) Then
            Alpha = True
        ElseIf Char.IsPunctuation(C) Then
            Punctuation = True
        End If
    Next C

    If Numeric AndAlso Alpha AndAlso Punctuation Then
        Return CharacterType.All
    ElseIf Numeric AndAlso Alpha AndAlso Not Punctuation Then
        Return CharacterType.AlphaNumeric
    ElseIf Alpha AndAlso Not Punctuation Then
        Return CharacterType.Alpha
    Else
        Return CharacterType.Numeric
    End If
End Function

I've tested with up to 100,000 simple iterations of the method:

Dim Suffix As String = String.Empty

For I As Integer = 0 To 100000
    Suffix = IncrementSequence(Suffix, CharacterType.Numeric)
    Console.WriteLine("New suffix: " & Suffix)
Next I

This produces results ending with 100001, as expected.

Changing to CharacterType.Alpha gives me an ending value of EQXE, while changing to CharacterType.AlphaNumeric gives me an ending value of 255T. (NGL, I'm not doing the math on that to confirm it's "right").

I've also (so far) successfully used this in my IncrementExistingFileName() method:

Public Shared Function IncrementExistingFileName(ByVal OriginalFileName As String, Optional ByVal SuffixStyle As CharacterType = CharacterType.Numeric, Optional ByVal SuffixLength As Integer = 2) As String
    Dim NewFileName As String = OriginalFileName
    Dim FilePath As String = Path.GetDirectoryName(OriginalFileName)
    Dim FileName As String = Path.GetFileNameWithoutExtension(OriginalFileName)
    Dim FileExt As String = Path.GetExtension(OriginalFileName)
    Dim Suffix As String = String.Empty

    While File.Exists(NewFileName)
        Suffix = IncrementSequence(Suffix, SuffixStyle, SuffixLength)

        NewFileName = $"{FilePath}\{FileName}_{Suffix}{FileExt}"
    End While

    Return NewFileName
End Function

As stated at the beginning of this question, I've looked at numerous examples of code, including the suggestion(s) from Incrementing a sequence of letters by one on this site, as well as a CodeProject article from 2006 that just didn't quite work right.

At this point, I guess I'm looking for some general feedback on the IncrementSequence() method. Am I "doing it right"? I mean, as I said, my testing so far indicates that it's working, but I'm aware that I still may have managed to overlook something "obvious" to someone else. Is there anything that I've done that could be done more effectively?


NOTE: I am aware that I deviate a bit from the .NET Naming Guidelines with regards to using PascalCase in instances where camelCase is preferred, but this is in line with my company's coding standards.

\$\endgroup\$
2
  • 1
    \$\begingroup\$ Is see only calls to IncrementSequence but not the declaration of it. \$\endgroup\$ Commented Aug 16, 2023 at 19:24
  • \$\begingroup\$ @OlivierJacot-Descombes - I'm sorry. I'm not sure why it's not showing up, but I have it in that first, big code block. Under the CharacterType enumeration definition there are some XML comments and then the IncrementSequence method definition starts with Public Shared Function IncrementSequence(ByVal CurrentValue As String, Optional ByVal SequenceStyle As CharacterType = CharacterType.NONE, Optional ByVal Length As Integer = 1, Optional ByVal ThrowOnInvalidChars As Boolean = False) As String.... \$\endgroup\$ Commented Aug 16, 2023 at 19:29

1 Answer 1

2
\$\begingroup\$

declarations

                DIM I As Integer = SequenceChars.Length - 1

Wow, that's super funny, guess we have to pick some way to make declarations. I remember Alcock teaching me DIM D(52) to allocate space for a deck of playing cards. Guess they didn't want to invent a new keyword when limiting a variable to int32. On the plus side we don't have to refer to $SequenceChars, so that's an improvement. Ok, sorry, moving on.


counting

The high level concept you should be pursuing here is that we have a serial, which we increment, and which we output in some convenient base.

Now, we could start incrementing from zero and output a base-2 binary result, or a base-10 decimal result. But here we wish to output a base-27 result.

So 0 is INVISIBLE SPACE (elided), and 1 .. 26 are "A" .. "Z". (I will represent INVISIBLE as _ underscore, to make it visible. Some folks might prefer ZWSP.) It lets us distinguish between A, AA, AAA and so on, in a way that zero would not.

One way to think of the decimal integer 2023 is: 0... ...000002023. That is, think of it as having an infinite prefix of zeros before it, which by convention we do not display.

Similarly unity in base-27 is ...00000A or ..._____A, with INVISIBLE zeros elided for display purposes.

Converting unsigned integer to its base-27 display form is straightforward.

When caller asks us to increment the integer, we should check to see if the display form contains any INVISIBLE characters that would not be displayed. If so we continue to increment, until obtaining an integer for which all digits (A..Z) shall be visibly displayed.

Consider 26 which displays as Z. Incrementing to 27 takes us from _Z to A_, which contains an INVISIBLE character that wants to be displayed but cannot. So we again increment to 28, or AA, a valid return value.


We don't have to use base-27, as base-26 could be made to work, with A being zero instead of one as above. We would need a string or a (length, value) tuple. Increment the length when counting past Z, ZZ, ZZZ etc.


magic numbers

                    ElseIf Alpha >= 65 AndAlso Alpha < 90 Then
                        ...
                    ElseIf Alpha >= 90 Then

I don't understand those comparisons at all. It looks buggy.

Rephrasing slightly the 65 comparison is Alpha >= ORD("A"), with a conjunct of Alpha < ORD("Z"). But we wanted <= there. Introducing magic numbers has obscured Author's Intent to the point where not even he can discern if there's a bug or not.

\$\endgroup\$
4
  • 2
    \$\begingroup\$ You are right, but we could go a step further and instead of declaring Alpha As Integer = Asc(SequenceChars(I)) just declare Alpha As Char = SequenceChars(I). Then we could compare Alpha >= "A"C. C denotes a character literal. \$\endgroup\$ Commented Aug 17, 2023 at 12:01
  • \$\begingroup\$ Thank you so much for your feedback. magic numbers - Good point. I see what you're saying and will make some adjustments to eliminate any "magic characters" (numbers, alpha, etc.). declarations - That's how we do in the VB world. Dim = var. What can I say? I know that it's technically possible to declare variables without using Dim, but that just ends up causing a whole lot more trouble than it's worth. \$\endgroup\$ Commented Aug 17, 2023 at 14:01
  • \$\begingroup\$ counting - Your explanation makes sense. I'm just having a difficult time "applying" the concept to the method where base 10, base 27, base 37, and base 65 come into play with slightly different "rules" - e.g., I don't need to "increment again" at the limit in base 10, but I do in base 27 (9=>10 vs Z=>AA). It gets even more complicated in the other sets where I'm not incrementing existing punctuation characters. This is why I went with a method where I'm not as much "counting" as doing character comparisons based on ASCII table values. \$\endgroup\$ Commented Aug 17, 2023 at 14:01
  • 1
    \$\begingroup\$ Well, suppose you wanted to use a set of wooden blocks, numbered 1 .. 9, to display a new number in the window each day. There's no zero block, so numbers like 10, 20, 100, & 1002 are simply not displayable. What do we do? We count (base ten) in the usual way, with +1 in the units place carrying over to tens place after 9, a +1 in tens place carrying into hundreds place, and so on. The counting is easy. For display, we skip (we keep counting) over undisplayable numbers that contain a zero. Same thing for your problem. Infinite leading zeros are fine, it's just the embedded ones we skip. \$\endgroup\$
    – J_H
    Commented Aug 17, 2023 at 16:14

Your Answer

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

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