snippetModerate
A CSharpish String.Format formatting helper
Viewed 0 times
formattingformathelperstringcsharpish
Problem
A while ago I implemented .net's
I'll start by listing a simple class called
...and the actual
```
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
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
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
Rewrite
Here's the refactored module-level function - it uses a
Escape Sequences
The
The factory
Added an
This puts escape sequences to be
- Each
Caseblock implements formatting functionality for a specific format specifier.
Gotostatements indicate the function wants to be broken down into several smaller functions.
- Local variables such as
alignmentSpecifier,alignmentPadding,precisionString,precisionSpecifier,formatSpecifierand all others, could all be eliminated if there was a concept of a "FormatSpecifier" object that held all these values.
- Bringing in
escapeHexand the C# hex specifier is a hack easily made useless by correctly encapsulating each format specifier.
escapescollection 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 FunctionEscape 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 PropertyThe 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 FunctionAdded 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 SubThis 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 FunctionPrivate 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 PropertyPublic 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 FunctionPublic 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 SubPrivate 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 PropertyContext
StackExchange Code Review Q#30817, answer score: 16
Revisions (0)
No revisions yet.