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.
IncrementSequencebut not the declaration of it. \$\endgroup\$CharacterTypeenumeration definition there are some XML comments and then theIncrementSequencemethod definition starts withPublic 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\$