Following up on this post, and including some major changes suggested, here's the revised code.
Changes include:
- No longer keeping an
IFormatProvider
at instance level. - Removed
IFormatProvider
constructor parameters. - Introduced
ToString(IFormatProvider)
overload. - Changed
decimal
tofloat
, to leveragefloat.NaN
for 0-denominator fractions. - Implemented more operators.
- Reimplemented
Equals
andCompareTo
per recommendations. - Fixed a bug in
ToString(string, IFormatProvider)
. - Added XML comments on all public members.
As the code file grew, it became apparent that I was going to need a way of grouping code sections. I did not want to use #region
because... it's a question of principles. It's irrational, I just don't want to use #region
.
So I regrouped all static
members into a partial struct
(note: some code lines and XML comments were reformatted to avoid horizontal scrolling):
/// <summary>
/// A fractional representation of a rational number.
/// </summary>
public partial struct Fraction
{
/// <summary>
/// An empty <c>Fraction</c> (0/0).
/// </summary>
public static readonly Fraction Empty = new Fraction();
/// <summary>
/// A <c>Fraction</c> representation of the integer value 0.
/// </summary>
public static readonly Fraction Zero = new Fraction(default(int));
/// <summary>
/// A <c>Fraction</c> representation of the integer value 1.
/// </summary>
public static readonly Fraction One = new Fraction(1);
/// <summary>
/// Represents the smallest possible value for a <see cref="Fraction"/>.
/// </summary>
public static readonly Fraction MinValue = new Fraction(1, int.MinValue);
/// <summary>
/// Represents the largest possible value for a <see cref="Fraction"/>.
/// </summary>
public static readonly Fraction MaxValue = new Fraction(int.MaxValue, 1);
/// <summary>
/// Returns a simplified/reduced representation of a fraction.
/// </summary>
/// <param name="fraction">The fraction to simplify.</param>
/// <returns>
/// Returns a new <see cref="Fraction"/>,
/// a simplified representation of this instance (if simplification is possible).
/// </returns>
public static Fraction Simplify(Fraction fraction)
{
if (fraction.IsUndefined)
{
return new Fraction(fraction);
}
var gcd= GetGreatestCommonDenominator(fraction.Numerator, fraction.Denominator);
var numerator = fraction.Numerator / gcd;
var denominator = fraction.Denominator / gcd;
return new Fraction(numerator, denominator);
}
private static int GetGreatestCommonDenominator(int numerator, int denominator)
{
return denominator == 0
? numerator
: GetGreatestCommonDenominator(denominator, numerator % denominator);
}
private static readonly Regex _parserRegex =
new Regex(@"^\s*?(?<numerator>\d+)\s*?/\s*?(?<denominator>\d+)\s*?$");
/// <summary>
/// Converts the string representation of a fraction
/// into its <c>Fraction</c> equivalent.
/// A return value indicates whether the conversion succeeded.
/// </summary>
/// <param name="s">A string containing the fraction to convert.</param>
/// <param name="result">
/// When this method returns, contains the <c>Fraction</c>
/// equivalent to the specified string.
/// </param>
/// <returns>Returns <c>true</c> if conversion is successful.</returns>
public static bool TryParse(string s, out Fraction result)
{
var syntaxMatch = _parserRegex.Match(s);
if (!syntaxMatch.Success)
{
result = Fraction.Zero;
return false;
}
var numerator = int.Parse(syntaxMatch.Groups["numerator"].Value);
var denominator = int.Parse(syntaxMatch.Groups["denominator"].Value);
result = new Fraction(numerator, denominator);
if (!result.IsUndefined)
{
result = result.Simplify();
}
return true;
}
public static explicit operator float(Fraction fraction)
{
return fraction.ToFloat();
}
public static bool operator ==(Fraction fraction1, Fraction fraction2)
{
return fraction1.Equals(fraction2);
}
public static bool operator ==(Fraction fraction, int value)
{
return fraction.Equals(new Fraction(value));
}
public static bool operator ==(Fraction fraction, float value)
{
Fraction result;
if (Fraction.TryParse(value.ToString(), out result))
{
return fraction.Equals(result);
}
return false;
}
public static bool operator !=(Fraction fraction1, Fraction fraction2)
{
return !(fraction1 == fraction2);
}
public static bool operator !=(Fraction fraction, int value)
{
return !(fraction == value);
}
public static bool operator !=(Fraction fraction, float value)
{
return !(fraction == value);
}
public static Fraction operator ++(Fraction fraction)
{
return new Fraction(fraction.Numerator + 1, fraction.Denominator);
}
public static Fraction operator +(Fraction fraction, int value)
{
return fraction + new Fraction(value);
}
public static Fraction operator +(Fraction fraction1, Fraction fraction2)
{
int numerator = (fraction1.Numerator * fraction2.Denominator)
+ (fraction1.Denominator * fraction2.Numerator);
int denominator = (fraction1.Denominator * fraction2.Denominator);
var result = new Fraction(numerator, denominator).Simplify();
return result;
}
public static Fraction operator --(Fraction fraction)
{
return new Fraction(fraction.Numerator - 1, fraction.Denominator);
}
public static Fraction operator -(Fraction fraction, int integer)
{
return fraction - new Fraction(integer);
}
public static Fraction operator -(Fraction fraction1, Fraction fraction2)
{
var subtrator = new Fraction(fraction2.Numerator * -1, fraction2.Denominator);
return fraction1 + subtrator;
}
public static Fraction operator /(Fraction fraction, int integer)
{
return fraction / new Fraction(integer);
}
public static Fraction operator /(Fraction fraction1, Fraction fraction2)
{
var divisor = new Fraction(fraction2.Denominator, fraction2.Numerator);
return fraction1 * divisor;
}
public static Fraction operator *(Fraction fraction, int integer)
{
return fraction * new Fraction(integer);
}
public static Fraction operator *(Fraction fraction1, Fraction fraction2)
{
var numerator = fraction1.Numerator * fraction2.Numerator;
var denominator = fraction1.Denominator * fraction2.Denominator;
var result = new Fraction(numerator, denominator).Simplify();
return result;
}
}
That left all instance members in their own file:
/// <summary>
/// A fractional representation of a rational number.
/// </summary>
[Serializable]
public partial struct Fraction : IFormattable,
IComparable,
IComparable<Fraction>,
IEquatable<Fraction>
{
private readonly int _numerator;
private readonly int _denominator;
/// <summary>
/// Copy constructor.
/// Creates a new <c>Fraction</c> instance based on the specified value.
/// </summary>
/// <param name="fraction"></param>
public Fraction(Fraction fraction)
: this(fraction.Numerator, fraction.Denominator)
{
}
/// <summary>
/// Creates a new <c>Fraction</c> with the denominator being 1.
/// </summary>
/// <param name="numerator"></param>
public Fraction(int numerator)
: this(numerator, 1)
{
}
/// <summary>
/// Creates a new <c>Fraction</c> with specified numerator and denominator.
/// </summary>
/// <param name="numerator"></param>
/// <param name="denominator"></param>
public Fraction(int numerator, int denominator)
{
_numerator = numerator;
_denominator = denominator;
}
/// <summary>
/// Gets the numerator (get-only).
/// </summary>
public int Numerator { get { return _numerator; } }
/// <summary>
/// Gets the denominator (get-only).
/// </summary>
public int Denominator { get { return _denominator; } }
/// <summary>
/// Gets a value indicating whether this instance is defined.
/// Returns true when the fraction is a division by zero.
/// </summary>
public bool IsUndefined { get { return _denominator == default(int); } }
/// <summary>
/// Simplifies/reduces the fraction.
/// </summary>
/// <returns>
/// Returns a simplified representation of this instance,
/// if simplification is possible.
/// </returns>
public Fraction Simplify()
{
return Fraction.Simplify(this);
}
/// <summary>
/// Creates a <c>float</c> representation of the <see cref="Fraction"/>.
/// </summary>
/// <returns>
/// Returns the result of dividing the <c>Numerator</c> by the <c>Denominator</c>,
/// or <c>float.NaN</c> when the <c>Denominator</c> is zero.
/// </returns>
public float ToFloat()
{
return IsUndefined ? float.NaN
: (float)_numerator / (float)_denominator;
}
/// <summary>
/// Returns a value indicating whether this instance and a specified object
/// represent the same value.
/// </summary>
/// <param name="obj">Any <c>Fraction</c> or <c>float</c>-convertible value.</param>
/// <returns></returns>
public override bool Equals(object obj)
{
if (obj is Fraction)
{
return Equals((Fraction)obj);
}
return ToFloat().Equals((float)obj);
}
/// <summary>
/// Returns the hash code for this instance.
/// </summary>
/// <returns></returns>
public override int GetHashCode()
{
return ToFloat().GetHashCode();
}
/// <summary>
/// Converts this fraction into a string representation,
/// using a default <see cref="FractionFormatter"/>.
/// </summary>
/// <returns></returns>
public override string ToString()
{
return ToString(FractionFormatter.Default);
}
/// <summary>
/// Converts this fraction into a string representation,
/// using specified <c>IFormatProvider</c>.
/// </summary>
/// <param name="provider"></param>
/// <returns></returns>
public string ToString(IFormatProvider provider)
{
return ToString(null, provider);
}
/// <summary>
/// Converts this fraction into a string representation,
/// using specified <c>format</c> and <c>IFormatProvider</c>.
/// </summary>
/// <param name="format"></param>
/// <param name="provider"></param>
/// <returns></returns>
public string ToString(string format, IFormatProvider provider)
{
if (provider is ICustomFormatter)
{
return ((ICustomFormatter)provider).Format(format, this, provider);
}
return FractionFormatter.Default.Format(format, this, FractionFormatter.Default);
}
/// <summary>
/// Compares this instance to a specified object and
/// returns an indication of their relative values.
/// </summary>
/// <param name="obj"></param>
/// <returns></returns>
public int CompareTo(object obj)
{
if (obj is int)
{
return CompareTo(new Fraction((int)obj));
}
else if (obj is string)
{
Fraction fraction;
if (Fraction.TryParse(obj as string, out fraction))
{
return CompareTo(fraction);
}
}
return CompareTo((Fraction)obj);
}
/// <summary>
/// Compares this instance to specified <c>Fraction</c> and
/// returns an indication of their relative values.
/// </summary>
/// <param name="other"></param>
/// <returns></returns>
public int CompareTo(Fraction other)
{
if (IsUndefined || other.IsUndefined)
{
// let the framework handle NaN comparisons
return ToFloat().CompareTo(other.ToFloat());
}
long left = _numerator * other.Denominator;
long right = _denominator * other.Numerator;
return left.CompareTo(right);
}
/// <summary>
/// Returns a value indicating whether
/// this instance is equal to a specified <c>Fraction</c> value.
/// </summary>
/// <param name="other"></param>
/// <returns></returns>
public bool Equals(Fraction other)
{
return CompareTo(other) == 0;
}
}
Here's a screenshot of a Solution Explorer view showing all members and overloads:
Is everything consistent? What could be improved? @mjolka suggested to write GetGreatestCommonDenominator
iteratively - I've found this code online and I find the recursive method is easier to read:
static int GetGreatestCommonDenominator(int numerator, int denominator) { int remainder; while (denominator != 0) { remainder = numerator % denominator; numerator = denominator; denominator = remainder; } return numerator; }
Am I sacrificing something here?
The FractionFormatter
has also undergone some minor changes, including it here for completion:
public class FractionFormatter : IFormatProvider, ICustomFormatter
{
private readonly CultureInfo _culture;
public FractionFormatter(CultureInfo culture)
{
_culture = culture;
}
public static FractionFormatter Default
{
get { return new FractionFormatter(CultureInfo.CurrentUICulture); }
}
public object GetFormat(Type formatType)
{
return (formatType == typeof(ICustomFormatter)) ? this : null;
}
public string Format(string format, object arg, IFormatProvider formatProvider)
{
var fraction = (Fraction)arg;
if (string.IsNullOrEmpty(format))
{
return string.Format(_culture, "{0}/{1}", fraction.Numerator, fraction.Denominator);
}
var result = string.Format(_culture, "{0:" + format + "}", fraction.ToFloat());
return result;
}
}
Same with MathJaxFractionFormatter
:
public class MathJaxFractionFormatter : IFormatProvider, ICustomFormatter
{
public enum MathJaxFractionSize
{
Normal,
Large
}
private static readonly CultureInfo _culture = typeof(FractionFormatter).Assembly.GetName().CultureInfo;
private readonly string _delimiter;
private readonly MathJaxFractionSize _size;
public MathJaxFractionFormatter()
: this("$", MathJaxFractionSize.Normal) { }
public MathJaxFractionFormatter(string delimiter, MathJaxFractionSize size)
{
_delimiter = delimiter;
_size = size;
}
public object GetFormat(Type formatType)
{
return (formatType == typeof(ICustomFormatter)) ? this : null;
}
public string Format(string format, object arg, IFormatProvider formatProvider)
{
var fraction = (Fraction)arg;
if (string.IsNullOrEmpty(format))
{
var keyword = _size == MathJaxFractionSize.Normal ? "\\frac" : "\\dfrac";
return string.Format(_culture, "{2}{3}{{{0}}}{{{1}}}{2}", fraction.Numerator, fraction.Denominator, _delimiter, keyword);
}
return fraction.ToString(format, _culture);
}
}
Now this will print a culture-sensitive 33.333 %
:
Console.WriteLine(new Fraction(1, 3).ToString("p3", new MathJaxFractionFormatter()));
And this will print $\frac{1}{3}$
as expected:
Console.WriteLine(new Fraction(1, 3).ToString(new MathJaxFractionFormatter()));
Passing in a CultureInfo.InvariantCulture
will simply cause ToString
to use FractionFormatter.Default
.
Console.WriteLine("5/25 with InvariantCulture: {0}",
new Fraction(5, 25).ToString(CultureInfo.InvariantCulture));
Outputs 5/25 with InvariantCulture: 5/25
.