Infix to Postfix in C#

A complete infix to postfix converter in C# using Stack<char>, plus a postfix evaluator. Both build with zero warnings on .NET 8 and were run before publishing.

The complete C# program

Save this as InfixToPostfix.cs. It uses only the standard library, handles multi-character operands, all six operators and nested brackets, and includes a postfix evaluator so you can check a conversion numerically.

InfixToPostfix.cs
// InfixToPostfix.cs — convert an infix expression to postfix (RPN)
// using Dijkstra's shunting-yard algorithm.
//
// Build and run:
//     dotnet new console -o InfixToPostfix
//     (replace Program.cs with this file)
//     dotnet run -- "A + B * C"

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text;

public static class InfixToPostfix
{
    // Binding power. Higher wins; equal levels are settled by associativity.
    private static readonly Dictionary<char, int> Precedence = new Dictionary<char, int>
    {
        ['+'] = 1, ['-'] = 1,
        ['*'] = 2, ['/'] = 2, ['%'] = 2,
        ['^'] = 3
    };

    // Only ^ groups right to left.
    private static bool IsRightAssociative(char op) => op == '^';

    private static bool IsOperandChar(char c) =>
        char.IsLetterOrDigit(c) || c == '_' || c == '.';

    /// <summary>Convert an infix expression to its postfix form.</summary>
    public static string Convert(string expression)
    {
        var output = new List<string>();
        var stack = new Stack<char>();

        for (int i = 0; i < expression.Length; i++)
        {
            char ch = expression[i];

            if (char.IsWhiteSpace(ch)) continue;

            // Operands may be several characters: 12, 3.5, count, x1.
            if (IsOperandChar(ch))
            {
                int start = i;
                while (i < expression.Length && IsOperandChar(expression[i])) i++;
                output.Add(expression.Substring(start, i - start));
                i--;
                continue;
            }

            if (ch == '(')
            {
                stack.Push(ch);
            }
            else if (ch == ')')
            {
                while (stack.Count > 0 && stack.Peek() != '(')
                    output.Add(stack.Pop().ToString());

                if (stack.Count == 0)
                    throw new FormatException("Unbalanced brackets: missing \"(\"");

                stack.Pop();               // discard the "("
            }
            else
            {
                if (!Precedence.TryGetValue(ch, out int thisPrecedence))
                    throw new FormatException($"Unexpected character '{ch}' at position {i}");

                while (stack.Count > 0)
                {
                    char top = stack.Peek();
                    if (top == '(') break;

                    bool higher = Precedence[top] > thisPrecedence;
                    bool equalLeft = Precedence[top] == thisPrecedence && !IsRightAssociative(ch);
                    if (!higher && !equalLeft) break;

                    output.Add(stack.Pop().ToString());
                }
                stack.Push(ch);
            }
        }

        while (stack.Count > 0)
        {
            char top = stack.Pop();
            if (top == '(')
                throw new FormatException("Unbalanced brackets: missing \")\"");
            output.Add(top.ToString());
        }

        return string.Join(" ", output);
    }

    /// <summary>Evaluate a postfix expression whose operands are numbers.</summary>
    public static double Evaluate(string postfix)
    {
        var stack = new Stack<double>();

        foreach (string token in postfix.Split(' ', StringSplitOptions.RemoveEmptyEntries))
        {
            if (token.Length == 1 && Precedence.ContainsKey(token[0]))
            {
                if (stack.Count < 2)
                    throw new FormatException("Malformed postfix expression");

                double b = stack.Pop();
                double a = stack.Pop();

                stack.Push(token[0] switch
                {
                    '+' => a + b,
                    '-' => a - b,
                    '*' => a * b,
                    '/' => a / b,
                    '%' => (long)a % (long)b,
                    '^' => Math.Pow(a, b),
                    _ => throw new FormatException("Unknown operator")
                });
                continue;
            }

            if (!double.TryParse(token, NumberStyles.Float, CultureInfo.InvariantCulture, out double number))
                throw new FormatException($"'{token}' is not a number");

            stack.Push(number);
        }

        if (stack.Count != 1)
            throw new FormatException("Malformed postfix expression");

        return stack.Pop();
    }

    public static void Main(string[] args)
    {
        string expression = args.Length > 0
            ? string.Join(" ", args)
            : "A + B * C - (D / E)";

        try
        {
            Console.WriteLine($"Infix:   {expression}");
            Console.WriteLine($"Postfix: {Convert(expression)}");
        }
        catch (FormatException e)
        {
            Console.Error.WriteLine($"error: {e.Message}");
            Environment.Exit(1);
        }
    }
}

Build and run it

dotnet new console -o InfixToPostfix
# replace Program.cs with the listing above
cd InfixToPostfix
dotnet run -- "A + B * C - (D / E)"

Sample output

Infix:   A + B * C - (D / E)
Postfix: A B C * + D E / -

Run it with no arguments and it converts that same default expression, which is a quick way to confirm the build works before trusting it with your own input.

How it works

The algorithm keeps two collections: an output list that only ever grows, and a stack holding operators and left brackets while they wait for their operands. Every token falls into one of four cases.

  • Operand — goes straight to the output; it never touches the stack.
  • Left bracket — pushed, where it acts as a fence nothing pops past.
  • Right bracket — pop to the output until the matching left bracket surfaces, then discard both. Brackets never appear in postfix.
  • Operator — pop while the operator on top binds at least as tightly, then push. That one condition is where precedence and associativity live.

Tokenizing as a separate step is what allows multi-character operands. Walking the string one character at a time would read x1 as two operands and quietly produce the wrong answer.

The stack in C#

Stack<char> is in System.Collections.Generic and does exactly what is needed: Push, Pop and Peek. Note that Pop both removes and returns, so the pop-to-output step is a single expression.

stack.Push(ch);                        // push
char top = stack.Peek();               // peek
output.Add(stack.Pop().ToString());    // pop straight into the output

What is idiomatic here

Switch expressions

The evaluator uses a switch expression rather than a statement, so each operator maps to a value with no assignment or fall-through to get wrong.

Culture-invariant parsing

double.TryParse is given CultureInfo.InvariantCulture. Without it, a machine with a comma decimal separator would reject 3.5.

The two rules that decide correctness

Nearly every wrong implementation gets one of these two wrong, and both live in the same loop condition.

Right-associativity of ^

A ^ B ^ C means A ^ (B ^ C), so the postfix form is A B C ^ ^ — not A B ^ C ^. That is why the loop pops on equal precedence only when the incoming operator is not right-associative.

Where % belongs

Modulo shares precedence with * and /, not with ^. Putting it on the wrong level changes the answer for any expression that mixes it with multiplication.

Evaluating the postfix result

Evaluation is the easier half: push operands, and on an operator pop two values, apply, push the result. Order matters — the second value popped is the left operand, which is what makes - and / come out the right way round. Converting and then evaluating 3 + 4 * 2 / (1 - 5) ^ 2 gives 3.5.

FAQ

Why the generic Stack and not the non-generic one?

The non-generic Stack stores object, so every character is boxed and every pop needs a cast. The generic version avoids both.

Does this handle multi-digit numbers and variable names?

Yes. IsOperandChar accepts letters, digits, underscore and dot, and the tokenizer consumes a whole run of them as one operand.

Will it run on .NET Framework?

The switch expression and Split overload need C# 8 and .NET Core 3.0 or newer. On older frameworks, replace the switch expression with a normal switch.

Check your output: paste any expression into the converter to see the same result with the full stack trace, or compare implementations in C · C++ · Python · Java · JavaScript · Go · Rust.