C# Generic Math

Karen Payne - Feb 26 '23 - - Dev Community

In this article generic math is used along with pattern matching to parse a string, assert there are five elements per and if so get the last three elements, determine if they are numeric.

Requires Visual Studio 2022 17.4 edition or higher

Rather than read from a file, data comes from mocked up data using raw string literal.

Sample data

internal class MockedData
{
    public static string FileDataForIntegers()
    {
        var lines = 
            """
            Karen,Payne,1,2,3
            May,Jones,4,b,x
            Jack,Smith,6,7,c
            ,,56,76,9
            """;

        return lines;
    }

    public static string FileDataForDecimals()
    {
        var lines =
            """
            Karen,Payne,10.5,2.3,3.45
            May,Jones,4.33,b,x
            Jack,Smith,16.8,7.48,c
            ,,56.88,76.4,9.999
            """;

        return lines;
    }
}
Enter fullscreen mode Exit fullscreen mode

Example 1

Using MockedData.FileDataForDecimals() get all decimals. The method below is generic which means it can be used for int, decimal etc.

If an element can not represent a numeric, 0 is used as the value.

public static T[] ToNumbersPreserveArray<T>(this string[] sender) where T : INumber<T>
{

    var array = Array.ConvertAll(sender, (input) =>
    {
        T.TryParse(input, NumberStyles.Any | 
                          NumberStyles.AllowDecimalPoint | 
                          NumberStyles.Float, CultureInfo.CurrentCulture, out var integerValue);
        return integerValue;
    });

    return array;

}
Enter fullscreen mode Exit fullscreen mode

Code which

  • Creates an anonymousItem type for each line, the line index and string array
  • Iterates each element in lines
  • Using pattern, skip first two elements, create a string array names values
  • Use the generic method ToNumbersPreserveArray to transform values array to decimal array

Note
Here anonymousItem is a tad overkill but will see it used in a upcoming code sample. Also, I'm sure a reader will think there is a better way and all bases are not covered, feel free to add that, here the topic is on generic math. Explore ListPatternApp for more on assertions.

public static void JustGetTheNumbers()
{
    Print();

    var lines = MockedData.FileDataForDecimals()
        .Split(Environment.NewLine)
        .Select((line, index) => new
        {
            Index = index, 
            Items = line.Split(',')
        })
        .ToArray();

    foreach (var anonymousItem in lines)
    {
        if (anonymousItem.Items is [_, _, .. var values])
        {
            var results = anonymousItem.Items.ToNumbersPreserveArray<decimal>();
            Console.WriteLine(string.Join(",", results.Skip(2)));
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Results, note the elements that do not represent a number are 0.

Just numbers result

Example 2

Same loading of data as the first example, difference is if a line values that were expected to by a decimal are flagged using GetNonNumericIndexes extension method.

public static void GetDecimals()
{
    Print();

    var lines = MockedData.FileDataForDecimals()
        .Split(Environment.NewLine)
        .Select((line, index) => new { Index = index, Items = line.Split(',') })
        .ToArray();

    foreach (var anonymousItem in lines)
    {
        if (anonymousItem.Items is [_, _, .. var values])
        {
            var results = values.GetNonNumericIndexes<decimal>();
            if (results.Length > 0)
            {
                Console.WriteLine(
                    $"Bad Line {anonymousItem.Index,-3}{string.Join(",", results),-5} -> {string.Join(",", anonymousItem.Items.Skip(2))}");
            }
            else
            {
                Console.WriteLine($"Line {anonymousItem.Index} is good");
                decimal[] numbers = anonymousItem.Items.ToNumberArray<decimal>();
                Console.WriteLine($"\t{string.Join(",", numbers)}");
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Extension method to obtain, in this case elements which could not represent a decimal.

public static int[] GetNonNumericIndexes<T>(this string[] sender) where T : INumber<T> =>
    sender.Select(
            (item, index) => T.TryParse(item, NumberStyles.Any | NumberStyles.AllowDecimalPoint, CultureInfo.CurrentCulture, out var _) ?
                new { IsNumber = true, Index = index } :
                new { IsNumber = false, Index = index })
        .ToArray()
        .Where(item => item.IsNumber == false)
        .Select(item => item.Index).ToArray();
Enter fullscreen mode Exit fullscreen mode

Results

result from code above

Since we are discussing generic math, the following uses the exact same extension, this time for int.

public static void GetIntegers()
{
    Print();

    var lines = MockedData.FileDataForIntegers()
        .Split(Environment.NewLine)
        .Select((line, index) => new { Index = index, Items = line.Split(',') })
        .ToArray();

    foreach (var anonymousItem in lines)
    {
        if (anonymousItem.Items is [_, _, .. var values])
        {
            var results = values.GetNonNumericIndexes<int>();
            if (results.Length > 0)
            {
                Console.WriteLine(
                    $"Bad Line {anonymousItem.Index,-3}{string.Join(",", results),-5} -> {string.Join(",", anonymousItem.Items.Skip(2))}");
            }
            else
            {
                Console.WriteLine($"Line {anonymousItem.Index} is good");
                int[] numbers = anonymousItem.Items.ToNumberArray<int>();
                Console.WriteLine($"\t{string.Join(",", numbers)}");
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Without generic math

We need an extension for each type to maintain.

public static decimal[] ToDecimalPreserveArray(this string[] sender)
{

    var decimalArray = Array.ConvertAll(sender, (input) =>
    {
        decimal.TryParse(input, out var decimalValue);
        return decimalValue;
    });

    return decimalArray;

}


public static int[] ToIntPreserveArray(this string[] sender)
{

    var intArray = Array.ConvertAll(sender, (input) =>
    {
        int.TryParse(input, out var intValue);
        return intValue;
    });

    return intArray;

}
Enter fullscreen mode Exit fullscreen mode

Considerations

There are advantages and disadvantages of generics including generic math and part of this can come from being stuck in old habits, unwilling to change are a few.

Consider on a similar note, generics can lessen code such having a generic between method.

public static class IComparableExtensions
{
    public static bool Between<T>(this T value, T lowerValue, T upperValue)
        where T : struct, IComparable<T>
        => Comparer<T>.Default.Compare(value, lowerValue) >= 0 &&
           Comparer<T>.Default.Compare(value, upperValue) <= 0;

    public static bool BetweenExclusive<T>(this IComparable<T> sender, T minimumValue, T maximumValue)
        => sender.CompareTo(minimumValue) > 0 && sender.CompareTo(maximumValue) < 0;

    public static bool IsGreaterThan<T>(this T sender, T value) where T : IComparable
        => sender.CompareTo(value) > 0;

    public static bool IsLessThan<T>(this T sender, T value) where T : IComparable
        => sender.CompareTo(value) < 0;
}
Enter fullscreen mode Exit fullscreen mode

Examples

The TimeOnly is from the .NET Framework while the others are from the extensions shown below where any object that implement IComparable can be used.

public static class IComparableExtensions
{
    public static bool Between<T>(this T value, T lowerValue, T upperValue)
        where T : struct, IComparable<T>
        => Comparer<T>.Default.Compare(value, lowerValue) >= 0 &&
           Comparer<T>.Default.Compare(value, upperValue) <= 0;

    public static bool BetweenExclusive<T>(this IComparable<T> sender, T minimumValue, T maximumValue)
        => sender.CompareTo(minimumValue) > 0 && sender.CompareTo(maximumValue) < 0;

    public static bool IsGreaterThan<T>(this T sender, T value) where T : IComparable
        => sender.CompareTo(value) > 0;

    public static bool IsLessThan<T>(this T sender, T value) where T : IComparable
        => sender.CompareTo(value) < 0;

    public static string CaseWhen(this int sender)
    {
        return sender switch
        {
            { } value1 and >= 7 and <= 14 => $"I am 7 or above but less than 14: {value1}",
            { } value2 when value2.Between(4, 6) => $"I am between 4 and 6: {value2}",
            { } value3 when (value3.IsLessThan(3)) => $"I am 3 or less: {value3}",
            _ => "I'm old"
        };
    }
}
Enter fullscreen mode Exit fullscreen mode

Let's try out these extension methods.

public static void BetweenExamples()
{

    Print();

    TimeOnly startTime = TimeOnly.Parse("11:00 PM");
    var hoursWorked = 2;
    TimeOnly endTime = startTime.AddHours(hoursWorked);

    // IsBetween here is native to TimeOnly
    var isBetween = TimeOnly.Parse("12:00 AM").IsBetween(startTime, endTime);
    Console.WriteLine($"12AM is between {startTime} and {endTime} -> {isBetween.ToYesNo()}");

    // uses our extension
    var age = 30;
    Console.WriteLine($"{age,-3} is over 30 {age.Between(30, 33).ToYesNo()}");

    DateTime lowDateTime = new(2022, 1, 1);
    DateTime someDateTime = new(2022, 1, 2);
    DateTime highDateTime = new(2022, 1, 8);

    // uses our extension switch expression
    Console.WriteLine($"{someDateTime:d} between {lowDateTime:d} and {highDateTime:d}? {someDateTime.Between(lowDateTime, highDateTime).ToYesNo()}");

    // uses our extension within a switch expression
    Console.WriteLine($"{7.CaseWhen()}");
    Console.WriteLine($"{5.CaseWhen()}");
    Console.WriteLine($"{1.CaseWhen()}");
    Console.WriteLine($"{16.CaseWhen()}");
}
Enter fullscreen mode Exit fullscreen mode

Results

results from code above

Summary

In this article I've touched on one benefit for using generics, there are others that can be found in the project GenericMathLibrary and GenericMathConsole projects while code presented here is in the project GenericMathListPatternConsoleApp.

Source code

Clone the following repository which was setup for when .NET Core 7 was released so there is more code than simply generic math.

Regarding generic math these extensions are included.

addition extension methods

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .