Jeffrey Sax wrote:
Operations on NaNs do not throw exceptions. Instead, so the IEEE-754 standard for binary floating-point arithmetic[^] states, the return value should be once again NaN.
Done. Changed Add and Multiply methods (used for all operations) to return NaN for operations involving left or right of NaN.
On that same note, you need to handle the special case that NaN != NaN is true. In fact, this is the only logical operation with a NaN operand that doesn't return false.
Done. Added a helper for comparing equality of two Fractions, made == and != call down to it. Double returns true from Equals if both values are NaN, so I do too.
Your implementation of Equals still doesn't comply with standard practices, although this is not stated as clearly as it should. Equals is used for object equality, and so should return false if the object being compared is not of type Fraction.
Done. The override now returns false for non-Fraction right-side, otherwise calls the new helper. Since Fraction is a value type, I wonder what object equality means in this case, so I just compare for same values after reduction.
Instead, the code in your Equals method belongs in a static, type safe Compare method that behaves like the Compare method of the IComparer interface. All logical operators should call this same method
Done. Added ICompare.CompareTo method. Since this is a value-type I made it handle the automatic conversions as appropriate. All the operator comparisons now use the type-safe CompareTo method (and internal helpers)
To be consistent with the rest of the Base Class Libraries, you may want to make these special values available as static readonly fields of the type
Done. Added Fraction.NaN, Fraction.PositiveInfinity and Fraction.NegativeInfinity. Also added MinValue, MaxValue and Epsilon. Also added Zero to deal with the issue of no-default constructor for structs (because it's a NaN otherwise).
You may also want to add the related static methods defined in Double, like IsPositiveInfinity, etc.
Those were already there. :grin: I did add IsInfinity
I also fixed the issues this code had with imprecise doubles (repeating decimals). I'm now using the repeating fraction logic I found here which does as good a job as possible and is relatively quick.
Here's the revised code:
using System;
using System.Runtime.InteropServices;
using System.Globalization;
namespace Mehroz
{
[Serializable, StructLayout(LayoutKind.Sequential)]
public struct Fraction : IComparable, IFormattable
{
#region Constructors
public Fraction(long wholeNumber)
{
if (wholeNumber == long.MinValue)
wholeNumber++;
m_Numerator = wholeNumber;
m_Denominator = 1;
}
public Fraction(double floatingPointNumber)
{
this = ToFraction(floatingPointNumber);
}
public Fraction(string inValue)
{
this = ToFraction(inValue);
}
public Fraction(long numerator, long denominator)
{
if (numerator == long.MinValue)
numerator++;
if (denominator == long.MinValue)
denominator++;
m_Numerator = numerator;
m_Denominator = denominator;
ReduceFraction(ref this);
}
private Fraction(Indeterminates type)
{
m_Numerator = (long)type;
m_Denominator = 0;
}
#endregion
#region Properties
public long Numerator
{
get
{
return m_Numerator;
}
set
{
m_Numerator = value;
}
}
public long Denominator
{
get
{
return m_Denominator;
}
set
{
m_Denominator = value;
}
}
#endregion
#region Expose constants
public static readonly Fraction NaN = new Fraction(Indeterminates.NaN);
public static readonly Fraction PositiveInfinity = new Fraction(Indeterminates.PositiveInfinity);
public static readonly Fraction NegativeInfinity = new Fraction(Indeterminates.NegativeInfinity);
public static readonly Fraction Zero = new Fraction(0,1);
public static readonly Fraction Epsilon = new Fraction(1, Int64.MaxValue);
private static readonly double EpsilonDouble = 1.0 / Int64.MaxValue;
public static readonly Fraction MaxValue = new Fraction(Int64.MaxValue, 1);
public static readonly Fraction MinValue = new Fraction(Int64.MinValue, 1);
#endregion
#region Explicit conversions
#region To primitives
public Int32 ToInt32()
{
if (this.m_Denominator == 0)
{
throw new FractionException(string.Format("Cannot convert {0} to Int32", IndeterminateTypeName(this.m_Numerator)), new System.NotFiniteNumberException());
}
long bestGuess = this.m_Numerator / this.m_Denominator;
if (bestGuess > Int32.MaxValue || bestGuess < Int32.MinValue)
{
throw new FractionException("Cannot convert to Int32", new System.OverflowException());
}
return (Int32)bestGuess;
}
public Int64 ToInt64()
{
if (this.m_Denominator == 0)
{
throw new FractionException(string.Format("Cannot convert {0} to Int64", IndeterminateTypeName(this.m_Numerator)), new System.NotFiniteNumberException());
}
return this.m_Numerator / this.m_Denominator;
}
public double ToDouble()
{
if (this.m_Denominator == 1)
return this.m_Numerator;
else if (this.m_Denominator == 0)
{
switch (NormalizeIndeterminate(this.m_Numerator))
{
case Indeterminates.NegativeInfinity:
return double.NegativeInfinity;
case Indeterminates.PositiveInfinity:
return double.PositiveInfinity;
case Indeterminates.NaN:
default:
return double.NaN;
}
}
else
{
return (double)this.m_Numerator / (double)this.m_Denominator;
}
}
public override string ToString()
{
if (this.m_Denominator == 1)
{
return this.m_Numerator.ToString();
}
else if (this.m_Denominator == 0)
{
return IndeterminateTypeName(this.m_Numerator);
}
else
{
return this.m_Numerator.ToString() + "/" + this.m_Denominator.ToString();
}
}
#endregion
#region From primitives
public static Fraction ToFraction(long inValue)
{
return new Fraction(inValue);
}
public static Fraction ToFraction(double inValue)
{
if (double.IsNaN(inValue))
return NaN;
else if (double.IsNegativeInfinity(inValue))
return NegativeInfinity;
else if (double.IsPositiveInfinity(inValue))
return PositiveInfinity;
else if (inValue == 0.0d)
return Zero;
if (inValue > Int64.MaxValue)
throw new OverflowException(string.Format("Double {0} too large", inValue));
if (inValue < -Int64.MaxValue)
throw new OverflowException(string.Format("Double {0} too small", inValue));
if (-EpsilonDouble < inValue && inValue < EpsilonDouble)
throw new ArithmeticException(string.Format("Double {0} cannot be represented", inValue));
int sign = Math.Sign(inValue);
inValue = Math.Abs(inValue);
return ConvertPositiveDouble(sign, inValue);
}
public static Fraction ToFraction(string inValue)
{
if (inValue == null || inValue == string.Empty)
throw new ArgumentNullException("inValue");
NumberFormatInfo info = NumberFormatInfo.CurrentInfo;
string trimmedValue = inValue.Trim();
if (trimmedValue == info.NaNSymbol)
return NaN;
else if (trimmedValue == info.PositiveInfinitySymbol)
return PositiveInfinity;
else if (trimmedValue == info.NegativeInfinitySymbol)
return NegativeInfinity;
else
{
int slashPos = inValue.IndexOf('/');
if (slashPos > -1)
{
long numerator = Convert.ToInt64(inValue.Substring(0, slashPos));
long denominator = Convert.ToInt64(inValue.Substring(slashPos + 1));
return new Fraction(numerator, denominator);
}
else
{
int decimalPos = inValue.IndexOf(info.CurrencyDecimalSeparator);
if (decimalPos > -1)
return new Fraction(Convert.ToDouble(inValue));
else
return new Fraction(Convert.ToInt64(inValue));
}
}
}
#endregion
#endregion
#region Indeterminate classifications
public bool IsNaN()
{
if (this.m_Denominator == 0
&& NormalizeIndeterminate(this.m_Numerator) == Indeterminates.NaN)
return true;
else
return false;
}
public bool IsInfinity()
{
if (this.m_Denominator == 0
&& NormalizeIndeterminate(this.m_Numerator) != Indeterminates.NaN)
return true;
else
return false;
}
public bool IsPositiveInfinity()
{
if (this.m_Denominator == 0
&& NormalizeIndeterminate(this.m_Numerator) == Indeterminates.PositiveInfinity)
return true;
else
return false;
}
public bool IsNegativeInfinity()
{
if (this.m_Denominator == 0
&& NormalizeIndeterminate(this.m_Numerator) == Indeterminates.NegativeInfinity)
return true;
else
return false;
}
#endregion
#region Inversion
public Fraction Inverse()
{
Fraction frac = new Fraction();
frac.m_Numerator = this.m_Denominator;
frac.m_Denominator = this.m_Numerator;
return frac;
}
public static Fraction Inverted(long value)
{
Fraction frac = new Fraction(value);
return frac.Inverse();
}
public static Fraction Inverted(double value)
{
Fraction frac = new Fraction(value);
return frac.Inverse();
}
#endregion
#region Operators
#region Unary Negation operator
public static Fraction operator -(Fraction left)
{
return Negate(left);
}
#endregion
#region Addition operators
public static Fraction operator +(Fraction left, Fraction right)
{
return Add(left, right);
}
public static Fraction operator +(long left, Fraction right)
{
return Add(new Fraction(left), right);
}
public static Fraction operator +(Fraction left, long right)
{
return Add(left, new Fraction(right));
}
public static Fraction operator +(double left, Fraction right)
{
return Add(ToFraction(left), right);
}
public static Fraction operator +(Fraction left, double right)
{
return Add(left, ToFraction(right));
}
#endregion
#region Subtraction operators
public static Fraction operator -(Fraction left, Fraction right)
{
return Add(left, - right);
}
public static Fraction operator -(long left, Fraction right)
{
return Add(new Fraction(left), - right);
}
public static Fraction operator -(Fraction left, long right)
{
return Add(left, new Fraction(- right));
}
public static Fraction operator -(double left, Fraction right)
{
return Add(ToFraction(left), - right);
}
public static Fraction operator -(Fraction left, double right)
{
return Add(left, ToFraction(- right));
}
#endregion
#region Multiplication operators
public static Fraction operator *(Fraction left, Fraction right)
{
return Multiply(left, right);
}
public static Fraction operator *(long left, Fraction right)
{
return Multiply(new Fraction(left), right);
}
public static Fraction operator *(Fraction left, long right)
{
return Multiply(left, new Fraction(right));
}
public static Fraction operator *(double left, Fraction right)
{
return Multiply(ToFraction(left), right);
}
public static Fraction operator *(Fraction left, double right)
{
return Multiply(left, ToFraction(right));
}
#endregion
#region Division operators
public static Fraction operator /(Fraction left, Fraction right)
{
return Multiply(left, right.Inverse());
}
public static Fraction operator /(long left, Fraction right)
{
return Multiply(new Fraction(left), right.Inverse());
}
public static Fraction operator /(Fraction left, long right)
{
return Multiply(left, Inverted(right));
}
public static Fraction operator /(double left, Fraction right)
{
return Multiply(ToFraction(left), right.Inverse());
}
public static Fraction operator /(Fraction left, double right)
{
return Multiply(left, Inverted(right));
}
#endregion
#region Modulus operators
public static Fraction operator %(Fraction left, Fraction right)
{
return Modulus(left, right);
}
public static Fraction operator %(long left, Fraction right)
{
return Modulus(new Fraction(left), right);
}
public static Fraction operator %(Fraction left, long right)
{
return Modulus(left, right);
}
public static Fraction operator %(double left, Fraction right)
{
return Modulus(ToFraction(left), right);
}
public static Fraction operator %(Fraction left, double right)
{
return Modulus(left, right);
}
#endregion
#region Equal operators
public static bool operator ==(Fraction left, Fraction right)
{
return left.CompareEquality(right, false);
}
public static bool operator ==(Fraction left, long right)
{
return left.CompareEquality(new Fraction(right), false);
}
public static bool operator ==(Fraction left, double right)
{
return left.CompareEquality(new Fraction(right), false);
}
#endregion
#region Not-equal operators
public static bool operator !=(Fraction left, Fraction right)
{
return left.CompareEquality(right, true);
}
public static bool operator !=(Fraction left, long right)
{
return left.CompareEquality(new Fraction(right), true);
}
public static bool operator !=(Fraction left, double right)
{
return left.CompareEquality(new Fraction(right), true);
}
#endregion
#region Inequality operators
public static bool operator <(Fraction left, Fraction right)
{
return left.CompareTo(right) < 0;
}
public static bool operator >(Fraction left, Fraction right)
{
return left.CompareTo(right) > 0;
}
public static bool operator <=(Fraction left, Fraction right)
{
return left.CompareTo(right) <= 0;
}
public static bool operator >=(Fraction left, Fraction right)
{
return left.CompareTo(right) >= 0;
}
#endregion
#region Implict conversion from primitive operators
public static implicit operator Fraction(long value)
{
return new Fraction(value);
}
public static implicit operator Fraction(double value)
{
return new Fraction(value);
}
public static implicit operator Fraction(string value)
{
return new Fraction(value);
}
#endregion
#region Explicit converstion to primitive operators
public static explicit operator int(Fraction frac)
{
return frac.ToInt32();
}
public static explicit operator long(Fraction frac)
{
return frac.ToInt64();
}
public static explicit operator double(Fraction frac)
{
return frac.ToDouble();
}
public static implicit operator string(Fraction frac)
{
return frac.ToString();
}
#endregion
#endregion
#region Equals and GetHashCode overrides
public override bool Equals(object obj)
{
if (obj == null || ! (obj is Fraction))
return false;
try
{
Fraction right = (Fraction)obj;
return this.CompareEquality(right, false);
}
catch
{
return false;
}
}
public override int GetHashCode()
{
ReduceFraction(ref this);
int numeratorHash = this.m_Numerator.GetHashCode();
int denominatorHash = this.m_Denominator.GetHashCode();
return (numeratorHash ^ denominatorHash);
}
#endregion
#region IComparable member and type-specific version
public int CompareTo(object obj)
{
if (obj == null)
return 1;
Fraction right;
if (obj is Fraction)
right = (Fraction)obj;
else if (obj is long)
right = (long)obj;
else if (obj is double)
right = (double)obj;
else if (obj is string)
right = (string)obj;
else
throw new ArgumentException("Must be convertible to Fraction", "obj");
return this.CompareTo(right);
}
public int CompareTo(Fraction right)
{
if (this.m_Denominator == 0)
{
return IndeterminantCompare(NormalizeIndeterminate(this.m_Numerator), right);
}
if (right.m_Denominator == 0)
{
return - IndeterminantCompare(NormalizeIndeterminate(right.m_Numerator), this);
}
CrossReducePair(ref this, ref right);
try
{
checked
{
long leftScale = this.m_Numerator * right.m_Denominator;
long rightScale = this.m_Denominator * right.m_Numerator;
if (leftScale < rightScale)
return -1;
else if (leftScale > rightScale)
return 1;
else
return 0;
}
}
catch (Exception e)
{
throw new FractionException(string.Format("CompareTo({0}, {1}) error", this, right), e);
}
}
#endregion
#region IFormattable Members
string System.IFormattable.ToString(string format, IFormatProvider formatProvider)
{
return this.m_Numerator.ToString(format, formatProvider) + "/" + this.m_Denominator.ToString(format, formatProvider);
}
#endregion
#region Reduction
public static void ReduceFraction(ref Fraction frac)
{
if (frac.m_Denominator == 0)
{
frac.m_Numerator = (long)NormalizeIndeterminate(frac.m_Numerator);
return;
}
if (frac.m_Numerator == 0)
{
frac.m_Denominator = 1;
return;
}
long iGCD = GCD(frac.m_Numerator, frac.m_Denominator);
frac.m_Numerator /= iGCD;
frac.m_Denominator /= iGCD;
if ( frac.m_Denominator < 0 )
{
frac.m_Numerator = - frac.m_Numerator;
frac.m_Denominator = - frac.m_Denominator;
}
}
public static void CrossReducePair(ref Fraction frac1, ref Fraction frac2)
{
if (frac1.m_Denominator == 0 || frac2.m_Denominator == 0)
return;
long gcdTop = GCD(frac1.m_Numerator, frac2.m_Denominator);
frac1.m_Numerator = frac1.m_Numerator / gcdTop;
frac2.m_Denominator = frac2.m_Denominator / gcdTop;
long gcdBottom = GCD(frac1.m_Denominator, frac2.m_Numerator);
frac2.m_Numerator = frac2.m_Numerator / gcdBottom;
frac1.m_Denominator = frac1.m_Denominator / gcdBottom;
}
#endregion
#region Implementation
#region Convert a double to a fraction
private static Fraction ConvertPositiveDouble(int sign, double inValue)
{
long fractionNumerator = (long)inValue;
double fractionDenominator = 1;
double previousDenominator = 0;
double remainingDigits = inValue;
int maxIterations = 594;
while (remainingDigits != Math.Floor(remainingDigits)
&& Math.Abs(inValue - (fractionNumerator / fractionDenominator)) > double.Epsilon)
{
remainingDigits = 1.0 / (remainingDigits - Math.Floor(remainingDigits));
double scratch = fractionDenominator;
fractionDenominator =(Math.Floor(remainingDigits) * fractionDenominator) + previousDenominator;
fractionNumerator = (long)(inValue * fractionDenominator + 0.5);
previousDenominator = scratch;
if (maxIterations-- < 0)
break;
}
return new Fraction(fractionNumerator * sign, (long)fractionDenominator);
}
#endregion
#region Equality helper
private bool CompareEquality(Fraction right, bool notEqualCheck)
{
ReduceFraction(ref this);
ReduceFraction(ref right);
if (this.m_Numerator == right.m_Numerator && this.m_Denominator == right.m_Denominator)
{
if (notEqualCheck && this.IsNaN())
return true;
else
return ! notEqualCheck;
}
else
{
return notEqualCheck;
}
}
#endregion
#region Comparison helper
private static int IndeterminantCompare(Indeterminates leftType, Fraction right)
{
switch (leftType)
{
case Indeterminates.NaN:
if (right.IsNaN())
return 0;
else if (right.IsNegativeInfinity())
return 1;
else
return -1;
case Indeterminates.NegativeInfinity:
if (right.IsNegativeInfinity())
return 0;
else
return -1;
case Indeterminates.PositiveInfinity:
if (right.IsPositiveInfinity())
return 0;
else
return 1;
default:
return 0;
}
}
#endregion
#region Math helpers
private static Fraction Negate(Fraction frac)
{
return new Fraction( - frac.m_Numerator, frac.m_Denominator);
}
private static Fraction Add(Fraction left, Fraction right)
{
if (left.IsNaN() || right.IsNaN())
return NaN;
long gcd = GCD(left.m_Denominator, right.m_Denominator);
long leftDenominator = left.m_Denominator / gcd;
long rightDenominator = right.m_Denominator / gcd;
try
{
checked
{
long numerator = left.m_Numerator * rightDenominator + right.m_Numerator * leftDenominator;
long denominator = leftDenominator * rightDenominator * gcd;
return new Fraction(numerator, denominator);
}
}
catch (Exception e)
{
throw new FractionException("Add error", e);
}
}
private static Fraction Multiply(Fraction left, Fraction right)
{
if (left.IsNaN() || right.IsNaN())
return NaN;
CrossReducePair(ref left, ref right);
try
{
checked
{
long numerator = left.m_Numerator * right.m_Numerator;
long denominator = left.m_Denominator * right.m_Denominator;
return new Fraction(numerator, denominator);
}
}
catch (Exception e)
{
throw new FractionException("Multiply error", e);
}
}
private static Fraction Modulus(Fraction left, Fraction right)
{
if (left.IsNaN() || right.IsNaN())
return NaN;
try
{
checked
{
Int64 quotient = (Int64)(left / right);
Fraction whole = new Fraction(quotient * right.m_Numerator, right.m_Denominator);
return left - whole;
}
}
catch (Exception e)
{
throw new FractionException("Modulus error", e);
}
}
private static long GCD(long left, long right)
{
if (left < 0)
left = - left;
if (right < 0)
right = - right;
if (left < 2 || right < 2)
return 1;
do
{
if (left < right)
{
long temp = left;
left = right;
right = temp;
}
left %= right;
} while (left != 0);
return right;
}
#endregion
#region Indeterminate helpers
private static string IndeterminateTypeName(long numerator)
{
System.Globalization.NumberFormatInfo info = NumberFormatInfo.CurrentInfo;
switch (NormalizeIndeterminate(numerator))
{
case Indeterminates.PositiveInfinity:
return info.PositiveInfinitySymbol;
case Indeterminates.NegativeInfinity:
return info.NegativeInfinitySymbol;
default:
case Indeterminates.NaN:
return info.NaNSymbol;
}
}
private static Indeterminates NormalizeIndeterminate(long numerator)
{
switch (Math.Sign(numerator))
{
case 1:
return Indeterminates.PositiveInfinity;
case -1:
return Indeterminates.NegativeInfinity;
default:
case 0:
return Indeterminates.NaN;
}
}
private enum Indeterminates
{
NaN = 0
, PositiveInfinity = 1
, NegativeInfinity = -1
}
#endregion
#region Member variables
private long m_Numerator;
private long m_Denominator;
#endregion
#endregion
}
public class FractionException : Exception
{
public FractionException(string Message, Exception InnerException) : base(Message, InnerException)
{
}
}
}
|