UpTo – Enumerating Characters with Extension Methods.
While randomly clicking through links online, I chanced upon an interesting piece of code at The Madman Scribblings. The first thing that interested me was that he had come up with an interesting variation to the If(Condition, Expression, Expression) operator that used Delegates for the expressions.
While I was try to think of ways this would be useful, I then came to the end of the short blog post, where he re-writes another method he had come up with, UpTo. This calculates a range of characters similar to how you can loop through a character range in C# using a for statement.
For those who haven't tried, C# treats Chars like integers and they can be traversed as such using code like this:
class Program
{
static void Main(string[] args)
{
for (char c = 'a'; c <= 'z'; c++)
Console.Write(c);
Console.WriteLine();
}
}
in VB, code like this does not work. I suppose the decision was made because historically the way to deal with characters and integers has always been using Chr and ChrW for finding the character corresponding to a particular integer value, and Asc and AscW for finding the numeric value that represents a particular character. Whatever the reasons, to achieve the same, one would have had to write code like this:
For c = AscW("a"c) To AscW("b"c)
Console.Write(ChrW(c))
Next
Console.WriteLine()
As you can see, you must first get the numeric values representing the characters, and in each iteration, you then calculate the corresponding character from the values you'd have obtained in the first step. C# is doing this for you in the background but this way of writing isn't too pretty, and this isn't the sort of thing you'd want to write multiple times if you need to calculate character ranges a lot. It's not something I do a lot so I just accepted the way VB does it, and just thought there must be some other way of doing it without writing this kind of loop all the time.
The UpTo extension method provides this very thing. It was so relevant to something I had long seen but never tackled that I was more interested in it than the main topic of the blog, the nifty If and IfElse extension methods.
Needless to say, I tried the code on my computer and came upon these minor issues.
- In UpTo, Math.Abs() was generating an overflow because the calculation handed to it resulted in a negative value which is invalid for a Byte. I figured he must have been using Option Strict Off so I used Type inference to make the types Integers instead of removing Option Strict.
- Inside IfElse, there was a syntax error at ElseThis) which I corrected this way elseThis(source)
- And the naming convention I use disallows underscores in method names so I removed these and the methods became If and IfElse instead of _If and _IfElse.
- I also shortened the code by using one line Lambda expressions instead of the multi-line versions.
Here is the result of my changes:
''' <summary>
''' Provides If(Boolean, Object) like functionality for IEnumerable(Of T) using a Delegate as the expression.
''' </summary>
''' <typeparam name="T">The type in the IEnumerable.</typeparam>
''' <param name="source">The source collection that we are looking at.</param>
''' <param name="condition">The condition with which to evaluate the source collection.</param>
''' <param name="doThis">Operation to perform if the condition is True.</param>
''' <returns>Result of doThis(source) if condition is True, source otherwise.</returns>
<Extension()>
Public Function [If](Of T)(ByVal source As IEnumerable(Of T), ByVal condition As Func(Of Boolean),
ByVal doThis As Func(Of IEnumerable(Of T), IEnumerable(Of T))) As IEnumerable(Of T)
Return If(condition(), doThis(source), source)
End Function
''' <summary>
''' Provides If(Boolean, Object, Object) like functionality for IEnumerable(Of T) using a Delegate as the expressions.
''' </summary>
''' <typeparam name="T">The type in the IEnumerable.</typeparam>
''' <param name="source">The source collection that we are evaluating.</param>
''' <param name="condition">The condition with which to evaluate the source collection.</param>
''' <param name="doThis">Operation to perform if the condition is True.</param>
''' <param name="elseThis">Operation to perform if the condition is False.</param>
''' <returns>Result of doThis(source) if condition evaluates to True, else the result of elseThis(source) is returned.</returns>
<Extension()>
Public Function IfElse(Of T)(ByVal source As IEnumerable(Of T), ByVal condition As Func(Of Boolean),
ByVal doThis As Func(Of IEnumerable(Of T), IEnumerable(Of T)),
ByVal elseThis As Func(Of IEnumerable(Of T), IEnumerable(Of T))) As IEnumerable(Of T)
Return If(condition(), doThis(source), elseThis(source))
End Function
''' <summary>
''' Calculates all the characters in the range fromChar to toChar inclusive.
''' </summary>
''' <param name="fromChar">The start character in the sequence.</param>
''' <param name="upToChar">The last character in the sequence.</param>
''' <returns>A string containing all the characters in the range.</returns>
''' <remarks>
''' If fromChar appears after upToChar in alphabetic order, the values are switched around so that an ordered list comes.
''' </remarks>
<Extension()>
Public Function UpTo(ByVal fromChar As Char, ByVal upToChar As Char) As String
Dim f = AscW(fromChar)
Dim u = AscW(upToChar)
Return Enumerable.Range({f, u}.Min, Math.Abs(u - f) + 1).
Select(Function(c) ChrW(c)).If(Function() f > u, Function(x) x.Reverse()).ToArray()
End Function
The above should be placed inside a Module. The following is an example of how to call this method.
Sub Main()
Dim startChar = "Z"c
Console.WriteLine(startChar.UpTo("A"c))
End Sub
I hope you find this as interesting as I did. Happy coding.