/* * MathConverter and accompanying samples are copyright (c) 2011 by Ivan Krivyakov * ivan [at] ikriv.com * They are distributed under the Apache License http://www.apache.org/licenses/LICENSE-2.0.html */ using System; using System.Collections.Generic; using System.Globalization; using System.Text.RegularExpressions; using System.Windows; using System.Windows.Data; using System.Windows.Markup; namespace IKriv.Wpf { /// /// Value converter that performs arithmetic calculations over its argument(s) /// /// /// MathConverter can act as a value converter, or as a multivalue converter (WPF only). /// It is also a markup extension (WPF only) which allows to avoid declaring resources, /// ConverterParameter must contain an arithmetic expression over converter arguments. Operations supported are +, -, * and / /// Single argument of a value converter may referred as x, a, or {0} /// Arguments of multi value converter may be referred as x,y,z,t (first-fourth argument), or a,b,c,d, or {0}, {1}, {2}, {3}, {4}, ... /// The converter supports arithmetic expressions of arbitrary complexity, including nested subexpressions /// public class MathConverter : #if !SILVERLIGHT MarkupExtension, IMultiValueConverter, #endif IValueConverter { Dictionary _storedExpressions = new Dictionary(); public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { return Convert(new object[] { value }, targetType, parameter, culture); } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { throw new NotImplementedException(); } public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) { try { decimal result = Parse(parameter.ToString()).Eval(values); if (targetType == typeof(decimal)) return result; if (targetType == typeof(string)) return result.ToString(); if (targetType == typeof(int)) return (int)result; if (targetType == typeof(double)) return (double)result; if (targetType == typeof(long)) return (long)result; throw new ArgumentException(String.Format("Unsupported target type {0}", targetType.FullName)); } catch (Exception ex) { ProcessException(ex); } return DependencyProperty.UnsetValue; } public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) { throw new NotImplementedException(); } #if !SILVERLIGHT public override object ProvideValue(IServiceProvider serviceProvider) { return this; } #endif protected virtual void ProcessException( Exception ex ) { Console.WriteLine(ex.Message); } private IExpression Parse(string s) { IExpression result = null; if (!_storedExpressions.TryGetValue(s, out result)) { result = new Parser().Parse(s); _storedExpressions[s] = result; } return result; } interface IExpression { decimal Eval(object[] args); } class Constant : IExpression { private decimal _value; public Constant(string text) { if (!decimal.TryParse(text, out _value)) { throw new ArgumentException(String.Format("'{0}' is not a valid number", text)); } } public decimal Eval(object[] args) { return _value; } } class Variable : IExpression { private int _index; public Variable(string text) { if (!int.TryParse(text, out _index) || _index<0) { throw new ArgumentException(String.Format("'{0}' is not a valid parameter index", text)); } } public Variable(int n) { _index = n; } public decimal Eval(object[] args) { if (_index >= args.Length) { throw new ArgumentException(String.Format("MathConverter: parameter index {0} is out of range. {1} parameter(s) supplied", _index, args.Length)); } return System.Convert.ToDecimal(args[_index]); } } class BinaryOperation : IExpression { private Func _operation; private IExpression _left; private IExpression _right; public BinaryOperation(char operation, IExpression left, IExpression right) { _left = left; _right = right; switch (operation) { case '+': _operation = (a, b) => (a + b); break; case '-': _operation = (a, b) => (a - b); break; case '*': _operation = (a, b) => (a * b); break; case '/': _operation = (a, b) => (a / b); break; default: throw new ArgumentException("Invalid operation " + operation); } } public decimal Eval(object[] args) { return _operation(_left.Eval(args), _right.Eval(args)); } } class Negate : IExpression { private IExpression _param; public Negate(IExpression param) { _param = param; } public decimal Eval(object[] args) { return -_param.Eval(args); } } class Parser { private string text; private int pos; public IExpression Parse(string text) { try { pos = 0; this.text = text; var result = ParseExpression(); RequireEndOfText(); return result; } catch (Exception ex) { string msg = String.Format("MathConverter: error parsing expression '{0}'. {1} at position {2}", text, ex.Message, pos); throw new ArgumentException(msg, ex); } } private IExpression ParseExpression() { IExpression left = ParseTerm(); while (true) { if (pos >= text.Length) return left; var c = text[pos]; if (c == '+' || c == '-') { ++pos; IExpression right = ParseTerm(); left = new BinaryOperation(c, left, right); } else { return left; } } } private IExpression ParseTerm() { IExpression left = ParseFactor(); while (true) { if (pos >= text.Length) return left; var c = text[pos]; if (c == '*' || c == '/') { ++pos; IExpression right = ParseFactor(); left = new BinaryOperation(c, left, right); } else { return left; } } } private IExpression ParseFactor() { SkipWhiteSpace(); if (pos >= text.Length) throw new ArgumentException("Unexpected end of text"); var c = text[pos]; if (c == '+') { ++pos; return ParseFactor(); } if (c == '-') { ++pos; return new Negate(ParseFactor()); } if (c == 'x' || c == 'a') return CreateVariable(0); if (c == 'y' || c == 'b') return CreateVariable(1); if (c == 'z' || c == 'c') return CreateVariable(2); if (c == 't' || c == 'd') return CreateVariable(3); if (c == '(') { ++pos; var expression = ParseExpression(); SkipWhiteSpace(); Require(')'); SkipWhiteSpace(); return expression; } if (c == '{') { ++pos; var end = text.IndexOf('}', pos); if (end < 0) { --pos; throw new ArgumentException("Unmatched '{'"); } if (end == pos) { throw new ArgumentException("Missing parameter index after '{'"); } var result = new Variable(text.Substring(pos, end - pos).Trim()); pos = end + 1; SkipWhiteSpace(); return result; } const string decimalRegEx = @"(\d+\.?\d*|\d*\.?\d+)"; var match = Regex.Match(text.Substring(pos), decimalRegEx); if (match.Success) { pos += match.Length; SkipWhiteSpace(); return new Constant(match.Value); } else { throw new ArgumentException(String.Format("Unexpeted character '{0}'", c)); } } private IExpression CreateVariable(int n) { ++pos; SkipWhiteSpace(); return new Variable(n); } private void SkipWhiteSpace() { while (pos < text.Length && Char.IsWhiteSpace((text[pos]))) ++pos; } private void Require(char c) { if (pos >= text.Length || text[pos] != c) { throw new ArgumentException("Expected '" + c + "'"); } ++pos; } private void RequireEndOfText() { if (pos != text.Length) { throw new ArgumentException("Unexpected character '" + text[pos] + "'"); } } } } }