HiveBrain v1.2.0
Get Started
← Back to all entries
snippetModerate

A CSharpish String.Format formatting helper

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
formattingformathelperstringcsharpish

Problem

A while ago I implemented .net's string.Format() method in VB6; it works amazingly well, but I'm sure there has to be a way to make it more efficient.

I'll start by listing a simple class called EscapeSequence:

Private Type tEscapeSequence
    EscapeString As String
    ReplacementString As String
End Type

Private this As tEscapeSequence
Option Explicit

Public Property Get EscapeString() As String
    EscapeString = this.EscapeString
End Property

Friend Property Let EscapeString(value As String)
    this.EscapeString = value
End Property

Public Property Get ReplacementString() As String
    ReplacementString = this.ReplacementString
End Property

Friend Property Let ReplacementString(value As String)
    this.ReplacementString = value
End Property

'Lord I wish VB6 had constructors!
Public Function Create(escape As String, replacement As String) As EscapeSequence
    Dim result As New EscapeSequence
    result.EscapeString = escape
    result.ReplacementString = replacement
    Set Create = result
End Function


...and the actual StringFormat function - there's a global variable PADDING_CHAR involved, which I'd love to find a way to specify and de-globalize:

```
Public Function StringFormat(format_string As String, ParamArray values()) As String
'VB6 implementation of .net String.Format(), slightly customized.

Dim return_value As String
Dim values_count As Integer

'some error-handling constants:
Const ERR_FORMAT_EXCEPTION As Long = vbObjectError Or 9001
Const ERR_ARGUMENT_NULL_EXCEPTION As Long = vbObjectError Or 9002
Const ERR_ARGUMENT_EXCEPTION As Long = vbObjectError Or 9003
Const ERR_SOURCE As String = "StringFormat"
Const ERR_MSG_INVALID_FORMAT_STRING As String = "Invalid format string."
Const ERR_MSG_FORMAT_EXCEPTION As String = "The number indicating an argument to format is less than zero, or greater than or equal to the length of the args array."
Const ER

Solution

Key Points

  • Each Case block implements formatting functionality for a specific format specifier.



  • Goto statements indicate the function wants to be broken down into several smaller functions.



  • Local variables such as alignmentSpecifier, alignmentPadding, precisionString, precisionSpecifier, formatSpecifier and all others, could all be eliminated if there was a concept of a "FormatSpecifier" object that held all these values.



  • Bringing in escapeHex and the C# hex specifier is a hack easily made useless by correctly encapsulating each format specifier.



  • escapes collection gets rebuilt every time the function is called, which is inefficient; valid escape sequences don't change from one call to the next.



  • ASCII (hex & octal) escapes both desperately want to be part of that collection.



  • Replacing \\ with ASCII code for Esc works nicely to get backslashes escaped.



Warning: below code is absolute overkill - no one in their right minds (I did this just for fun!) would do all this just to format strings in their VB6 or VBA application. However it shows how the monolithic function can be refactored to remove all Select...Case blocks and Goto statements.

Rewrite

Here's the refactored module-level function - it uses a Private helper As New StringHelper, declared at module level ("declarations" section):

Public Function StringFormat(format_string As String, ParamArray values()) As String
    Dim valuesArray() As Variant
    valuesArray = values
    StringFormat = helper.StringFormat(format_string, valuesArray)
End Function


Escape Sequences

The EscapeSequence class was annoyingly leaving out ASCII escapes, so I tackled this first:

Private Type tEscapeSequence
    EscapeString As String
    ReplacementString As String
    IsAsciiCharacter As Boolean
    AsciiBase As AsciiEscapeBase
End Type

Public Enum AsciiEscapeBase
    Octal
    Hexadecimal
End Enum

Private this As tEscapeSequence
Option Explicit

Public Property Get EscapeString() As String
    EscapeString = this.EscapeString
End Property

Friend Property Let EscapeString(value As String)
    this.EscapeString = value
End Property

Public Property Get ReplacementString() As String
    ReplacementString = this.ReplacementString
End Property

Friend Property Let ReplacementString(value As String)
    this.ReplacementString = value
End Property

Public Property Get IsAsciiCharacter() As Boolean
    IsAsciiCharacter = this.IsAsciiCharacter
End Property

Friend Property Let IsAsciiCharacter(value As Boolean)
    this.IsAsciiCharacter = value
End Property

Public Property Get AsciiBase() As AsciiEscapeBase
    AsciiBase = this.AsciiBase
End Property

Friend Property Let AsciiBase(value As AsciiEscapeBase)
    this.AsciiBase = value
End Property


The factory Create function was added two optional parameters; one to specify whether the escape sequence indicates an ASCII replacement escape, the other to specify the base (an enum) of the digits representing the ASCII code:

Public Function Create(escape As String, replacement As String, _
                       Optional ByVal isAsciiReplacement As Boolean = False, _
                       Optional ByVal base As AsciiEscapeBase = Octal) As EscapeSequence

    Dim result As New EscapeSequence

    result.EscapeString = escape
    result.ReplacementString = replacement
    result.IsAsciiCharacter = isAsciiReplacement
    result.AsciiBase = base

    Set Create = result

End Function


Added an Execute method here - all escape sequences boil down to the same thing: *replace the EscapeString with the ReplacementString, so we might as well encapsulate it here. ASCII escapes are a little bit more complex so I put them in their own method:

Public Sub Execute(ByRef string_value As String)

    If this.IsAsciiCharacter Then
        ProcessAsciiEscape string_value, this.EscapeString

    ElseIf StringContains(string_value, this.EscapeString) Then
        string_value = Replace(string_value, this.EscapeString, this.ReplacementString)

    End If

End Sub

Private Sub ProcessAsciiEscape(ByRef format_string As String, _
                               ByVal regexPattern As String)

    Dim regex As RegExp, matches As MatchCollection, thisMatch As Match
    Dim prefix As String, char As Long

    If Not StringContains(format_string, "\") Then Exit Sub

    Set regex = New RegExp
    regex.pattern = regexPattern
    regex.IgnoreCase = True
    regex.Global = True

    Select Case this.AsciiBase
        Case AsciiEscapeBase.Octal
            prefix = "&O"

        Case AsciiEscapeBase.Hexadecimal
            prefix = "&H"

    End Select

    Set matches = regex.Execute(format_string)        
    For Each thisMatch In matches
        char = CLng(prefix & thisMatch.SubMatches(0))
        format_string = Replace(format_string, thisMatch.value, Chr$(char))

    Next

    Set regex = Nothing
    Set matches = Nothing

End Sub


This puts escape sequences to be

Code Snippets

Public Function StringFormat(format_string As String, ParamArray values()) As String
    Dim valuesArray() As Variant
    valuesArray = values
    StringFormat = helper.StringFormat(format_string, valuesArray)
End Function
Private Type tEscapeSequence
    EscapeString As String
    ReplacementString As String
    IsAsciiCharacter As Boolean
    AsciiBase As AsciiEscapeBase
End Type

Public Enum AsciiEscapeBase
    Octal
    Hexadecimal
End Enum

Private this As tEscapeSequence
Option Explicit

Public Property Get EscapeString() As String
    EscapeString = this.EscapeString
End Property

Friend Property Let EscapeString(value As String)
    this.EscapeString = value
End Property

Public Property Get ReplacementString() As String
    ReplacementString = this.ReplacementString
End Property

Friend Property Let ReplacementString(value As String)
    this.ReplacementString = value
End Property

Public Property Get IsAsciiCharacter() As Boolean
    IsAsciiCharacter = this.IsAsciiCharacter
End Property

Friend Property Let IsAsciiCharacter(value As Boolean)
    this.IsAsciiCharacter = value
End Property

Public Property Get AsciiBase() As AsciiEscapeBase
    AsciiBase = this.AsciiBase
End Property

Friend Property Let AsciiBase(value As AsciiEscapeBase)
    this.AsciiBase = value
End Property
Public Function Create(escape As String, replacement As String, _
                       Optional ByVal isAsciiReplacement As Boolean = False, _
                       Optional ByVal base As AsciiEscapeBase = Octal) As EscapeSequence

    Dim result As New EscapeSequence

    result.EscapeString = escape
    result.ReplacementString = replacement
    result.IsAsciiCharacter = isAsciiReplacement
    result.AsciiBase = base

    Set Create = result

End Function
Public Sub Execute(ByRef string_value As String)

    If this.IsAsciiCharacter Then
        ProcessAsciiEscape string_value, this.EscapeString

    ElseIf StringContains(string_value, this.EscapeString) Then
        string_value = Replace(string_value, this.EscapeString, this.ReplacementString)

    End If

End Sub

Private Sub ProcessAsciiEscape(ByRef format_string As String, _
                               ByVal regexPattern As String)

    Dim regex As RegExp, matches As MatchCollection, thisMatch As Match
    Dim prefix As String, char As Long

    If Not StringContains(format_string, "\") Then Exit Sub

    Set regex = New RegExp
    regex.pattern = regexPattern
    regex.IgnoreCase = True
    regex.Global = True

    Select Case this.AsciiBase
        Case AsciiEscapeBase.Octal
            prefix = "&O"

        Case AsciiEscapeBase.Hexadecimal
            prefix = "&H"

    End Select

    Set matches = regex.Execute(format_string)        
    For Each thisMatch In matches
        char = CLng(prefix & thisMatch.SubMatches(0))
        format_string = Replace(format_string, thisMatch.value, Chr$(char))

    Next

    Set regex = Nothing
    Set matches = Nothing

End Sub
Private Type tSpecifier
    Index As Integer
    identifier As String
    AlignmentSpecifier As Integer
    PrecisionSpecifier As Integer
    CustomSpecifier As String
End Type

Private this As tSpecifier
Option Explicit

Public Property Get Index() As Integer
    Index = this.Index
End Property

Public Property Let Index(value As Integer)
    this.Index = value
End Property   

Public Property Get identifier() As String
    identifier = this.identifier
End Property

Public Property Let identifier(value As String)
    this.identifier = value
End Property

Public Property Get Alignment() As Integer
    Alignment = this.AlignmentSpecifier
End Property

Public Property Let Alignment(value As Integer)
    this.AlignmentSpecifier = value
End Property

Public Property Get Precision() As Integer
    Precision = this.PrecisionSpecifier
End Property

Public Property Get CustomSpecifier() As String
    CustomSpecifier = this.CustomSpecifier
End Property

Public Property Let CustomSpecifier(value As String)
    this.CustomSpecifier = value
    If IsNumeric(value) And val(value) <> 0 Then this.PrecisionSpecifier = CInt(value)
End Property

Context

StackExchange Code Review Q#30817, answer score: 16

Revisions (0)

No revisions yet.