While on a project creating a WPF application for some real heavy Excel users, I’ve received a lot of requests to make it more like Excel. One of the requirements that came in was to allow the users to type in fractions and view values as fractions in the WPF textboxes for values that were doubles in the model and in the database.
To fulfill this request, I needed a few things:
- I needed to be able to find approximate fractional values for doubles.
- I needed to validate and convert input in fractional format into a double.
- I needed to output double values in a mixed fraction format.
Finding approximate fractional values for doubles
To handle this, I created a Fraction struct that has four properties. IsPositive, WholeNumber, Numerator, and Denominator. The meat of the struct is a static method called Parse that accepts a double and returns a Fraction. It calls the private static method ApproximateFraction below:
/// <summary> /// Approximates the provided value to a fraction. /// </summary> /// <param name="value">The double being approximated as a fraction.</param> /// <param name="precision">Maximum difference targeted for the fraction to be considered equal to the value.</param> /// <returns>Fraction struct representing the value.</returns> private static Fraction ApproximateFraction(double value, double precision) { bool positive = value > 0; int wholeNumber = 0; int numerator = 1; int denominator = 1; double fraction = numerator / denominator; while (System.Math.Abs(fraction - value) > precision) { if (fraction < value) { numerator++; } else { denominator++; numerator = (int)System.Math.Round(value * denominator); } fraction = numerator / (double)denominator; } if (numerator < 0) numerator = numerator * -1; while (numerator >= denominator) { wholeNumber += 1; numerator -= denominator; } return new Fraction(positive, wholeNumber, numerator, denominator); }
Validate and convert fractional formatted inputs into doubles
This part actually ended up being the ConvertBack method in the IValueConverter created to bind to TextBoxes. The basic idea here was to first try and parse the value as a double. If it failed, I checked to see if it matched my regular expression for a mixed fraction. If it did, I parsed that mixed fraction out and made a Fraction struct. If not, I throw a FormatException.
/// <summary> /// Converts a string into a nullable double /// </summary> /// <param name="value">A string value that should be transformable into a double</param> /// <param name="targetType"></param> /// <param name="parameter"></param> /// <param name="culture"></param> /// <returns></returns> public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { if (value != null && !string.IsNullOrEmpty(value.ToString())) { double rValue; string rawValue = value.ToString().Trim(); rawValue = rawValue.Replace("- ", "-"); while(rawValue.Contains(" ")) { rawValue = rawValue.Replace(" ", " "); } // Regular Expression that represents a number in Fraction format. Regex FractionRegex = new Regex(@"^-?([0-9]* )?[0-9]+/[0-9]+$"); // If the value can be parsed as a double, do it and return if (double.TryParse(rawValue, out rValue)) { return rValue; } // Else if the value can be read as a fractional value, extract the number and return the a double from it. else if (FractionRegex.IsMatch(rawValue)) { // Check to see if the input if (FractionRegex.IsMatch(rawValue)) { try { Regex numeratorRegex = new Regex(@"(\s|^|-)[0-9]+\/"); bool isNegative; int wholeNumber; int numerator; int denominator; isNegative = rawValue.StartsWith("-"); wholeNumber = Math.Abs(rawValue.Any(x => x == ' ') ? int.Parse(rawValue.Remove(rawValue.IndexOf(" "))) : 0); denominator = int.Parse(rawValue.Substring(rawValue.LastIndexOf("/") + 1)); numerator = Math.Abs(int.Parse((numeratorRegex.Match(rawValue)).Value.Replace("/", ""))); return new Fraction(!isNegative, wholeNumber, numerator, denominator).ToDouble(); } catch { throw new FormatException(String.Format("Invalid Format: {0} cannot be converted to a numeric value.", value.ToString())); } } } // This value could not be parsed as a double and didn't match a fraction using our Fractional Regular Expression, throw a FormatException. else { throw new FormatException(String.Format("Invalid Format: {0} cannot be converted to a numeric value.", value.ToString())); } } return null; }
Output double values in a mixed fraction format
An finally the Convert method of the IValueConverter used to display doubles bound to string properties such as TextBox.Text or TextBlock.Text. In this method I have a couple of things going on, but first things first, I change the double into an instance of the Fraction struct. Using the ConverterParameter, I allow the developer to choose whether or not they want to restrict the use of fractional output to certain denominators. There are four possible return paths:
- Null, in the case the nullable double is null.
- Mixed fraction format when denominators are being restricted.
- The decimal format when denominators are being restricted and the fraction’s denominator isn’t in the allowed list.
- Mixed fraction format when denominators aren’t being restricted.
/// <summary> /// Converts a double into a string. /// </summary> /// <param name="value">A nullable double to get converted into a string</param> /// <param name="targetType"></param> /// <param name="parameter">true or false, determines if we use the denominator restrictions</param> /// <param name="culture"></param> /// <returns>A string representing the double value, maybe in decimal format, maybe in fractional format.</returns> public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { string decimalFormatString = "0.######"; bool restrictDenominator = false; if (parameter != null) { if (!bool.TryParse(parameter.ToString(), out restrictDenominator)) { restrictDenominator = false; } } double? dValue = value as Nullable<double>; if (restrictDenominator && dValue != null && dValue.HasValue) { Fraction asFraction = Fraction.Parse(dValue.Value); var validDenominators = new List<int>(new int[] { 2,3,4,5,6,7,8,9,16,32,64 }); if (validDenominators.Contains(asFraction.Denominator)) { return asFraction.ToString(validDenominators, decimalFormatString); } else { return dValue.Value.ToString(decimalFormatString); } } else if (!dValue.HasValue) { return dValue; } else { Fraction asFraction = Fraction.Parse(dValue.Value); return asFraction.ToString(null, String.Empty); } }
Using the Converter in your WPF Application
The last thing of course would be to use the converter in your application. If you haven’t used converters in WPF yet, it is fairly easy. First you need to declare an XML namespace pointing at the .NET namespace where your converters live (see the line in the code sample below that starts “xmlns:converter”). Next you need to declare a resource for your specific converter (See the one line in “Window.Resources”). Finally you create a binding using the converter which is done below in the TextBox Text property.
<Window x:Class="Andora.BlogSample.FractionTextBoxes.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:converter="clr-namespace:Andora.BlogSample.FractionTextBoxes.Converters" Title="MainWindow" Height="350" Width="525"> <Window.Resources> <converter:FractionConverter x:Key="fractionConverter" /> </Window.Resources> <Grid Margin="15"> <Grid.RowDefinitions> <RowDefinition /> <RowDefinition /> </Grid.RowDefinitions> <StackPanel> <Label Content="Enter a numeric in decimal or fraction format: " /> <TextBox Text="{Binding MyDouble, Converter={StaticResource ResourceKey=fractionConverter}, ConverterParameter=true}" Height="25" />
Conclusion
If you are developing in an environment where the users are accustomed to viewing and entering data in fraction formats, without too much hassle, you can provide users of your WPF applications the ability to do exactly that. You can find a full working sample with code all zipped up using the link below. Happy coding!!