Edit

Share via


Match data against patterns

This tutorial teaches you how to use pattern matching to inspect data in C#. You write small amounts of code, then you compile and run that code. The tutorial contains a series of lessons that explore different kinds of types in C#. These lessons teach you the fundamentals of the C# language.

Tip

When a code snippet block includes the "Run" button, that button opens the interactive window, or replaces the existing code in the interactive window. When the snippet doesn't include a "Run" button, you can copy the code and add it to the current interactive window.

The preceding tutorials demonstrated built-in types and types you define as tuples or records. Instances of these types can be checked against a pattern. Whether an instance matches a pattern determines the actions your program takes. Let's start to explore how you can use patterns.

Match a value

All the examples in this tutorial use text input that represents a series of bank transactions as comma separated values (CSV) input. In each of the samples you can match the record against a pattern using either an is or switch expression. This first example splits each line on the , character and then matches the first string field against the value "DEPOSIT" or "WITHDRAWAL" using an is expression. When it matches, the transaction amount is added or deducted from the current account balance. To see it work, press the "Run" button:

string bankRecords = """
    DEPOSIT,   10000, Initial balance
    DEPOSIT,     500, regular deposit
    WITHDRAWAL, 1000, rent
    DEPOSIT,    2000, freelance payment
    WITHDRAWAL,  300, groceries
    DEPOSIT,     700, gift from friend
    WITHDRAWAL,  150, utility bill
    DEPOSIT,    1200, tax refund
    WITHDRAWAL,  500, car maintenance
    DEPOSIT,     400, cashback reward
    WITHDRAWAL,  250, dining out
    DEPOSIT,    3000, bonus payment
    WITHDRAWAL,  800, loan repayment
    DEPOSIT,     600, stock dividends
    WITHDRAWAL,  100, subscription fee
    DEPOSIT,    1500, side hustle income
    WITHDRAWAL,  200, fuel expenses
    DEPOSIT,     900, refund from store
    WITHDRAWAL,  350, shopping
    DEPOSIT,    2500, project milestone payment
    WITHDRAWAL,  400, entertainment
    """;

double currentBalance = 0.0;
var reader = new StringReader(bankRecords);

string? line;
while ((line = reader.ReadLine()) is not null)
{
    if (string.IsNullOrWhiteSpace(line)) continue;
    // Split the line based on comma delimiter and trim each part
    string[] parts = line.Split(',');

    string? transactionType = parts[0]?.Trim();
    if (double.TryParse(parts[1].Trim(), out double amount))
    {
        // Update the balance based on transaction type
        if (transactionType?.ToUpper() is "DEPOSIT")
            currentBalance += amount;
        else if (transactionType?.ToUpper() is "WITHDRAWAL")
            currentBalance -= amount;

        Console.WriteLine($"{line.Trim()} => Parsed Amount: {amount}, New Balance: {currentBalance}");
    }
}

Examine the output. You can see that each line is processed by comparing the value of the text in the first field. The preceding sample could be similarly constructed using the == operator to test that two string values are equal. Comparing a variable to a constant is a basic building block for pattern matching. Let's explore more of the building blocks that are part of pattern matching.

Enum matches

Another common use for pattern matching is to match on the values of an enum type. This next sample processes the input records to create a tuple where the first value is an enum value that notes a deposit or a withdrawal. The second value is the value of the transaction. To see it work, press the "Run" button:

Warning

Don't copy and paste. The interactive window must be reset to run the following samples. If you make a mistake, the window hangs, and you need to refresh the page to continue.

public static class ExampleProgram
{
    const string bankRecords = """
    DEPOSIT,   10000, Initial balance
    DEPOSIT,     500, regular deposit
    WITHDRAWAL, 1000, rent
    DEPOSIT,    2000, freelance payment
    WITHDRAWAL,  300, groceries
    DEPOSIT,     700, gift from friend
    WITHDRAWAL,  150, utility bill
    DEPOSIT,    1200, tax refund
    WITHDRAWAL,  500, car maintenance
    DEPOSIT,     400, cashback reward
    WITHDRAWAL,  250, dining out
    DEPOSIT,    3000, bonus payment
    WITHDRAWAL,  800, loan repayment
    DEPOSIT,     600, stock dividends
    WITHDRAWAL,  100, subscription fee
    DEPOSIT,    1500, side hustle income
    WITHDRAWAL,  200, fuel expenses
    DEPOSIT,     900, refund from store
    WITHDRAWAL,  350, shopping
    DEPOSIT,    2500, project milestone payment
    WITHDRAWAL,  400, entertainment
    """;

    public static void Main()
    {
        double currentBalance = 0.0;

        foreach (var transaction in TransactionRecords(bankRecords))
        {
            if (transaction.type == TransactionType.Deposit)
                currentBalance += transaction.amount;
            else if (transaction.type == TransactionType.Withdrawal)
                currentBalance -= transaction.amount;
            Console.WriteLine($"{transaction.type} => Parsed Amount: {transaction.amount}, New Balance: {currentBalance}");
        }
    }

    static IEnumerable<(TransactionType type, double amount)> TransactionRecords(string inputText)
    {
        var reader = new StringReader(inputText);
        string? line;
        while ((line = reader.ReadLine()) is not null)
        {
            string[] parts = line.Split(',');

            string? transactionType = parts[0]?.Trim();
            if (double.TryParse(parts[1].Trim(), out double amount))
            {
                // Update the balance based on transaction type
                if (transactionType?.ToUpper() is "DEPOSIT")
                    yield return (TransactionType.Deposit, amount);
                else if (transactionType?.ToUpper() is "WITHDRAWAL")
                    yield return (TransactionType.Withdrawal, amount);
            }
            yield return (TransactionType.Invalid, 0.0);
        }
    }
}

public enum TransactionType
{
    Deposit,
    Withdrawal,
    Invalid
}

The preceding example also uses an if statement to check the value of an enum expression. Another form of pattern matching uses a switch expression. Let's explore that syntax and how you can use it.

Exhaustive matches with switch

A series of if statements can test a series of conditions. But, the compiler can't tell if a series of if statements are exhaustive or if later if conditions are subsumed by earlier conditions. The switch expression ensures both of those characteristics are met, which results in fewer bugs in your apps. Let's try it and experiment. Copy the following code. Replace the two if statements in the interactive window with the switch expression you copied. After you've modified the code, press the "Run" button at the top of the interactive window to run the new sample.

currentBalance += transaction switch
{
    (TransactionType.Deposit, var amount) => amount,
    (TransactionType.Withdrawal, var amount) => -amount,
    _ => 0.0,
};

When you run the code, you see that it works the same. To demonstrate subsumption, reorder the switch arms as shown in the following snippet:

currentBalance += transaction switch
{
    (TransactionType.Deposit, var amount) => amount,
    _ => 0.0,
    (TransactionType.Withdrawal, var amount) => -amount,
};

After you reorder the switch arms, press the "Run" button. The compiler issues an error because the arm with _ matches every value. As a result, that final arm with TransactionType.Withdrawal never runs. The compiler tells you that something's wrong in your code.

The compiler issues a warning if the expression tested in a switch expression could contain values that don't match any switch arm. If some values could fail to match any condition, the switch expression isn't exhaustive. The compiler also issues a warning if some values of the input don't match any of the switch arms. For example, if you remove the line with _ => 0.0,, any invalid values don't match. At run time, that would fail. Once you install the .NET SDK and build programs in your environment, you can test this behavior. The online experience doesn't display warnings in the output window.

Type patterns

To finish this tutorial, let's explore one more building block to pattern matching: the type pattern. A type pattern tests an expression at run time to see if it's the specified type. You can use a type test with either an is expression or a switch expression. Let's modify the current sample in two ways. First, instead of a tuple, let's build Deposit and Withdrawal record types that represent the transactions. Add the following declarations at the bottom of the interactive window:

public record Deposit(double Amount, string description);
public record Withdrawal(double Amount, string description);

Next, add this method after the Main method to parse the text and return a series of records:

public static IEnumerable<object?> TransactionRecordType(string inputText)
{
    var reader = new StringReader(inputText);
    string? line;
    while ((line = reader.ReadLine()) is not null)
    {
        string[] parts = line.Split(',');

        string? transactionType = parts[0]?.Trim();
        if (double.TryParse(parts[1].Trim(), out double amount))
        {
            // Update the balance based on transaction type
            if (transactionType?.ToUpper() is "DEPOSIT")
                yield return new Deposit(amount, parts[2]);
            else if (transactionType?.ToUpper() is "WITHDRAWAL")
                yield return new Withdrawal(amount, parts[2]);
        }
        yield return default;
    }
}

Finally, replace the foreach loop in the Main method with the following code:

foreach (var transaction in TransactionRecordType(bankRecords))
{
    currentBalance += transaction switch
    {
        Deposit d => d.Amount,
        Withdrawal w => -w.Amount,
        _ => 0.0,
    };
    Console.WriteLine($" {transaction} => New Balance: {currentBalance}");
}

Then, press the "Run" button to see the results. This final version tests the input against a type.

Pattern matching provides a vocabulary to compare an expression against characteristics. Patterns can include the expression's type, values of types, property values, and combinations of them. Comparing expressions against a pattern can be more clear than multiple if comparisons. You explored some of the patterns you can use to match expressions. There are many more ways to use pattern matching in your applications. First, visit the .NET site to download the .NET SDK, create a project on your machine, and keep coding. As you explore, you can learn more about pattern matching in C# in the following articles: