The Math Converter

Quite often I have wanted to adjust a bound value in my XAML by a mathematical formula.

For example, I might want to size a control to 50% of it’s parent’s size. Or I’ll want to position it 30% down and 20% left of a parent panel. Or I might even want to make a control’s width something like 50% of (WindowHeight – 200). Of course, I could always write converters for this, but I got tired of doing that repeatedly so sat down one day to write one big MathConverter.

Using the Math Converter

Using the MathConverter is simple. All you have to do is pass the Converter a math equation as the ConverterParameter, substituting @VALUE for your bound value. The equation can consist of numbers, basic operators (+, -, *, /, %), parentheses, and @VALUE.

For example, if you wanted a control whose height was 50% of the current Window Size, you would create a binding that looked like this:

Height="{Binding ElementName=RootWindow, Path=ActualHeight,
                 Converter={StaticResource MathConverter},
                 ConverterParameter=@VALUE/2}"

Or if you wanted to make the control’s width equal to 30% of (WindowWidth – 200) your binding might look like this:

Width="{Binding ElementName=RootWindow, Path=ActualWidth,
                Converter={StaticResource MathConverter},
                ConverterParameter=((@VALUE-200)*.3)}"

The Math Converter

Here’s the actual converter code.

It takes the ConverterParameter, substitutes the bound value for @VALUE, then starts reading the equation from left-to-right. If it encounters an operator, it performs the specified operation on the previous number and the next number in the list. If it encounters a parentheses, it handles the equation inside the group first, then replaces the grouped equation in the string with the result of the grouped equation. Any character it finds that is not a number, operator, or parentheses throws an exception.

// Does a math equation on the bound value.
// Use @VALUE in your mathEquation as a substitute for bound value
// Operator order is parenthesis first, then Left-To-Right (no operator precedence)
public class MathConverter : IValueConverter
{
    private static readonly char[] _allOperators = new[] { '+', '-', '*', '/', '%', '(', ')' };

    private static readonly List<string> _grouping = new List<string> { "(", ")" };
    private static readonly List<string> _operators = new List<string> { "+", "-", "*", "/", "%" };

    #region IValueConverter Members

    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        // Parse value into equation and remove spaces
        var mathEquation = parameter as string;
        mathEquation = mathEquation.Replace(" ", "");
        mathEquation = mathEquation.Replace("@VALUE", value.ToString());

        // Validate values and get list of numbers in equation
        var numbers = new List<double>();
        double tmp;

        foreach (string s in mathEquation.Split(_allOperators))
        {
            if (s != string.Empty)
            {
                if (double.TryParse(s, out tmp))
                {
                    numbers.Add(tmp);
                }
                else
                {
                    // Handle Error - Some non-numeric, operator, or grouping character found in string
                    throw new InvalidCastException();
                }
            }
        }

        // Begin parsing method
        EvaluateMathString(ref mathEquation, ref numbers, 0);

        // After parsing the numbers list should only have one value - the total
        return numbers[0];
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }

    #endregion

    // Evaluates a mathematical string and keeps track of the results in a List<double> of numbers
    private void EvaluateMathString(ref string mathEquation, ref List<double> numbers, int index)
    {
        // Loop through each mathemtaical token in the equation
        string token = GetNextToken(mathEquation);

        while (token != string.Empty)
        {
            // Remove token from mathEquation
            mathEquation = mathEquation.Remove(0, token.Length);

            // If token is a grouping character, it affects program flow
            if (_grouping.Contains(token))
            {
                switch (token)
                {
                    case "(":
                        EvaluateMathString(ref mathEquation, ref numbers, index);
                        break;

                    case ")":
                        return;
                }
            }

            // If token is an operator, do requested operation
            if (_operators.Contains(token))
            {
                // If next token after operator is a parenthesis, call method recursively
                string nextToken = GetNextToken(mathEquation);
                if (nextToken == "(")
                {
                    EvaluateMathString(ref mathEquation, ref numbers, index + 1);
                }

                // Verify that enough numbers exist in the List<double> to complete the operation
                // and that the next token is either the number expected, or it was a ( meaning
                // that this was called recursively and that the number changed
                if (numbers.Count > (index + 1) &&
                    (double.Parse(nextToken) == numbers[index + 1] || nextToken == "("))
                {
                    switch (token)
                    {
                        case "+":
                            numbers[index] = numbers[index] + numbers[index + 1];
                            break;
                        case "-":
                            numbers[index] = numbers[index] - numbers[index + 1];
                            break;
                        case "*":
                            numbers[index] = numbers[index] * numbers[index + 1];
                            break;
                        case "/":
                            numbers[index] = numbers[index] / numbers[index + 1];
                            break;
                        case "%":
                            numbers[index] = numbers[index] % numbers[index + 1];
                            break;
                    }
                    numbers.RemoveAt(index + 1);
                }
                else
                {
                    // Handle Error - Next token is not the expected number
                    throw new FormatException("Next token is not the expected number");
                }
            }

            token = GetNextToken(mathEquation);
        }
    }

    // Gets the next mathematical token in the equation
    private string GetNextToken(string mathEquation)
    {
        // If we're at the end of the equation, return string.empty
        if (mathEquation == string.Empty)
        {
            return string.Empty;
        }

        // Get next operator or numeric value in equation and return it
        string tmp = "";
        foreach (char c in mathEquation)
        {
            if (_allOperators.Contains(c))
            {
                return (tmp == "" ? c.ToString() : tmp);
            }
            else
            {
                tmp += c;
            }
        }

        return tmp;
    }
}

One day I plan on expanding this further to use an IMultiValueConverter that accepts multiple bindings, but I haven’t had a need for that yet so it hasn’t gotten done.

18 Responses to The Math Converter

  1. Eric Mink says:

    Thanks so much, Rachel!

    For others who would like to use this with Xamarin Forms, the changes required are only imports and adding a ConvertBack method which can throw a NotImplementedException.

  2. Timmy Fuller says:

    Hi Rachel, This converter is Great. Nice Job. I noticed a small defect in it. when I pass in have an equation like “(@VALUE*(4/10))”. To fix it simply check the nextToken first in the following bit of code:

    // Verify that enough numbers exist in the List to complete the operation
    // and that the next token is either the number expected, or it was a ( meaning
    // that this was called recursively and that the number changed
    if (numbers.Count > (index + 1) &&
    nextToken == “(” || double.Parse(nextToken) == numbers[index + 1])
    {

    Thanks Again.
    Tim

  3. Simon says:

    Hi Rachel, thanks for your great code! What’s the code’s license, may we use it in a commercial project? šŸ™‚

    • Rachel says:

      Hi Simon, I have no license for my code. Its nice if there’s a link pointing to where you got it from, but all the code I post on here is relatively simple and could easily be reproduced by another developer looking to accomplish the same thing, so I don’t feel the need to license any of it. Thanks for asking though!

  4. millerni456 says:

    Excellent job! By the way, I was able to trigger the InvalidCastExpection because the number passed was “9.124678E”. This value is from a bound double property and the fomula was only “@VALUE*50”. I believe the solution is to use NumberStyles.Float in the double.TryParse. Hope that helps!

  5. tommyHU says:

    Great stuff. šŸ™‚

    One more question: can this be extended to handle multiple binding? For example, to be able to use @VALUE, @VALUE2, @VALUE3 in the expression? Thanks.

    • Rachel says:

      Yes, its easy to change this to an IMultiValueConverter and do a .Replace on each @VALUE#. The only thing to remember is loop backwards through the @VALUES so you don’t accidently replace something like @VALUE10 with @VALUE1.

      I actually have the code for my MultiValueConverter on Stack Overflow here if you want.

  6. Bruce says:

    Thanks so much for providing this; it worked great!

  7. mbv800 says:

    Can the following equation be included? (@VALUE*(-1))

    • Rachel says:

      Hrrmm it would probably throw an exception on that one because there is no number before the operator, but that could be easy to fix. Would probably need to modify the if statement before the switch(token) in EvaluateMathString and have it use a 0 by default if no number is found on the left side of the operator.

  8. Sebastien says:

    I added the following operators :

    case “>”:
    numbers[index] = Math.Max(numbers[index] , numbers[index + 1]);
    break;
    case “0<500 will keep the value between 0 and 500

  9. olandol says:

    Good code and thank you.

  10. Andy says:

    Nice one!

  11. Mike says:

    Really useful, I can now forget entirely about converters! Thank you

  12. Saskia says:

    Thank you Rachel, very helpful!

  13. This code doesn’t compile. _allOperators.Contains(c) is not valid.

Leave a comment