#region License /*---------------------------------------------------------------------------------*\ Distributed under the terms of an MIT-style license: The MIT License Copyright (c) 2005-2010 Stephen M. McKamey Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \*---------------------------------------------------------------------------------*/ #endregion License using System; using System.Collections.Generic; using System.Reflection; namespace ExifUtils { /// /// Represents a rational number /// [Serializable] public struct Rational : IConvertible, IComparable, IComparable where T : IConvertible { #region Delegate Types private delegate T ParseDelegate(string value); private delegate bool TryParseDelegate(string value, out T rational); #endregion Delegate Types #region Constants private const char Delim = '/'; private static readonly char[] DelimSet = new char[] { Delim }; #endregion Constants #region Fields public static readonly Rational Empty = new Rational(); private static ParseDelegate Parser; private static TryParseDelegate TryParser; private static decimal maxValue; private readonly T numerator; private readonly T denominator; #endregion Fields #region Init /// /// Ctor /// /// The numerator of the rational number. /// The denominator of the rational number. /// reduces by default public Rational(T numerator, T denominator) : this(numerator, denominator, false) { } /// /// Ctor /// /// The numerator of the rational number. /// The denominator of the rational number. /// determines if should reduce by greatest common divisor public Rational(T numerator, T denominator, bool reduce) { this.numerator = numerator; this.denominator = denominator; if (reduce) { Rational.Reduce(ref this.numerator, ref this.denominator); } } #endregion Init #region Properties /// /// Gets and sets the numerator of the rational number /// public T Numerator { get { return this.numerator; } } /// /// Gets and sets the denominator of the rational number /// public T Denominator { get { return this.denominator; } } /// /// Gets a value indicating if this is an empty instance /// public bool IsEmpty { get { return this.Equals(Rational.Empty); } } /// /// Gets the MaxValue /// private static decimal MaxValue { get { if (Rational.maxValue == default(decimal)) { FieldInfo maxValue = typeof(T).GetField("MaxValue", BindingFlags.Static|BindingFlags.Public); if (maxValue != null) { try { Rational.maxValue = Convert.ToDecimal(maxValue.GetValue(null)); } catch (OverflowException) { Rational.maxValue = Decimal.MaxValue; } } else { Rational.maxValue = Int32.MaxValue; } } return Rational.maxValue; } } #endregion Properties #region Parse Methods /// /// Approximate the decimal value accurate to a precision of 0.000001m /// /// decimal value to approximate /// an approximation of the value as a rational number /// /// http://stackoverflow.com/questions/95727 /// public static Rational Approximate(decimal value) { return Rational.Approximate(value, 0.000001m); } /// /// Approximate the decimal value accurate to a certain precision /// /// decimal value to approximate /// maximum precision to converge /// an approximation of the value as a rational number /// /// http://stackoverflow.com/questions/95727 /// public static Rational Approximate(decimal value, decimal epsilon) { decimal numerator = Decimal.Truncate(value); decimal denominator = Decimal.One; decimal fraction = Decimal.Divide(numerator, denominator); decimal maxValue = Rational.MaxValue; while (Math.Abs(fraction-value) > epsilon && (denominator < maxValue) && (numerator < maxValue)) { if (fraction < value) { numerator++; } else { denominator++; decimal temp = Math.Round(Decimal.Multiply(value, denominator)); if (temp > maxValue) { denominator--; break; } numerator = temp; } fraction = Decimal.Divide(numerator, denominator); } return new Rational( (T)Convert.ChangeType(numerator, typeof(T)), (T)Convert.ChangeType(denominator, typeof(T))); } /// /// Converts the string representation of a number to its equivalent. /// /// /// public static Rational Parse(string value) { if (String.IsNullOrEmpty(value)) { return Rational.Empty; } if (Rational.Parser == null) { Rational.Parser = Rational.BuildParser(); } string[] parts = value.Split(Rational.DelimSet, 2, StringSplitOptions.RemoveEmptyEntries); T numerator = Rational.Parser(parts[0]); T denominator; if (parts.Length > 1) { denominator = Rational.Parser(parts[1]); } else { denominator = default(T); } return new Rational(numerator, denominator); } /// /// Converts the string representation of a number to its equivalent. /// A return value indicates whether the conversion succeeded or failed. /// /// /// /// public static bool TryParse(string value, out Rational rational) { if (String.IsNullOrEmpty(value)) { rational = Rational.Empty; return false; } if (Rational.TryParser == null) { Rational.TryParser = Rational.BuildTryParser(); } T numerator, denominator; string[] parts = value.Split(Rational.DelimSet, 2, StringSplitOptions.RemoveEmptyEntries); if (!Rational.TryParser(parts[0], out numerator)) { rational = Rational.Empty; return false; } if (parts.Length > 1) { if (!Rational.TryParser(parts[1], out denominator)) { rational = Rational.Empty; return false; } } else { denominator = default(T); } rational = new Rational(numerator, denominator); return (parts.Length == 2); } private static Rational.ParseDelegate BuildParser() { MethodInfo parse = typeof(T).GetMethod( "Parse", BindingFlags.Public|BindingFlags.Static, null, new Type[] { typeof(string) }, null); if (parse == null) { throw new InvalidOperationException("Underlying Rational type T must support Parse in order to parse Rational."); } return new Rational.ParseDelegate( delegate(string value) { try { return (T)parse.Invoke(null, new object[] { value }); } catch (TargetInvocationException ex) { if (ex.InnerException != null) { throw ex.InnerException; } throw; } }); } private static Rational.TryParseDelegate BuildTryParser() { // http://stackoverflow.com/questions/1933369 MethodInfo tryParse = typeof(T).GetMethod( "TryParse", BindingFlags.Public|BindingFlags.Static, null, new Type[] { typeof(string), typeof(T).MakeByRefType() }, null); if (tryParse == null) { throw new InvalidOperationException("Underlying Rational type T must support TryParse in order to try-parse Rational."); } return new Rational.TryParseDelegate( delegate(string value, out T output) { object[] args = new object[] { value, default(T) }; try { bool success = (bool)tryParse.Invoke(null, args); output = (T)args[1]; return success; } catch (TargetInvocationException ex) { if (ex.InnerException != null) { throw ex.InnerException; } throw; } }); } #endregion Parse Methods #region Math Methods /// /// Finds the greatest common divisor and reduces the fraction by this amount. /// /// the reduced rational public Rational Reduce() { T numerator = this.numerator; T denominator = this.denominator; Rational.Reduce(ref numerator, ref denominator); return new Rational(numerator, denominator); } /// /// Finds the greatest common divisor and reduces the fraction by this amount. /// /// the reduced rational private static void Reduce(ref T numerator, ref T denominator) { bool reduced = false; decimal n = Convert.ToDecimal(numerator); decimal d = Convert.ToDecimal(denominator); // greatest common divisor decimal gcd = Rational.GCD(n, d); if (gcd != Decimal.One && gcd != Decimal.Zero) { reduced = true; n /= gcd; d /= gcd; } // cancel out signs if (d < Decimal.Zero) { reduced = true; n = -n; d = -d; } if (reduced) { numerator = (T)Convert.ChangeType(n, typeof(T)); denominator = (T)Convert.ChangeType(d, typeof(T)); } } /// /// Lowest Common Denominator /// /// /// /// private static decimal LCD(decimal a, decimal b) { if (a == Decimal.Zero && b == Decimal.Zero) { return Decimal.Zero; } return (a * b) / Rational.GCD(a, b); } /// /// Greatest Common Devisor /// /// /// /// private static decimal GCD(decimal a, decimal b) { if (a < Decimal.Zero) { a = -a; } if (b < Decimal.Zero) { b = -b; } while (a != b) { if (a == Decimal.Zero) { return b; } if (b == Decimal.Zero) { return a; } if (a > b) { a %= b; } else { b %= a; } } return a; } #endregion Math Methods #region IConvertible Members /// /// /// /// /// public string ToString(IFormatProvider provider) { return String.Concat( this.numerator.ToString(provider), Rational.Delim, this.denominator.ToString(provider)); } /// /// /// /// /// public decimal ToDecimal(IFormatProvider provider) { try { decimal denominator = this.denominator.ToDecimal(provider); if (denominator == Decimal.Zero) { return Decimal.Zero; } return this.numerator.ToDecimal(provider) / denominator; } catch (InvalidCastException) { long denominator = this.denominator.ToInt64(provider); if (denominator == 0L) { return 0L; } return ((IConvertible)this.numerator.ToInt64(provider)).ToDecimal(provider) / ((IConvertible)denominator).ToDecimal(provider); } } /// /// /// /// /// public double ToDouble(IFormatProvider provider) { double denominator = this.denominator.ToDouble(provider); if (denominator == 0.0) { return 0.0; } return this.numerator.ToDouble(provider) / denominator; } /// /// /// /// /// public float ToSingle(IFormatProvider provider) { float denominator = this.denominator.ToSingle(provider); if (denominator == 0.0f) { return 0.0f; } return this.numerator.ToSingle(provider) / denominator; } bool IConvertible.ToBoolean(IFormatProvider provider) { return ((IConvertible)this.ToDecimal(provider)).ToBoolean(provider); } byte IConvertible.ToByte(IFormatProvider provider) { return ((IConvertible)this.ToDecimal(provider)).ToByte(provider); } char IConvertible.ToChar(IFormatProvider provider) { return ((IConvertible)this.ToDecimal(provider)).ToChar(provider); } short IConvertible.ToInt16(IFormatProvider provider) { return ((IConvertible)this.ToDecimal(provider)).ToInt16(provider); } int IConvertible.ToInt32(IFormatProvider provider) { return ((IConvertible)this.ToDecimal(provider)).ToInt32(provider); } long IConvertible.ToInt64(IFormatProvider provider) { return ((IConvertible)this.ToDecimal(provider)).ToInt64(provider); } sbyte IConvertible.ToSByte(IFormatProvider provider) { return ((IConvertible)this.ToDecimal(provider)).ToSByte(provider); } ushort IConvertible.ToUInt16(IFormatProvider provider) { return ((IConvertible)this.ToDecimal(provider)).ToUInt16(provider); } uint IConvertible.ToUInt32(IFormatProvider provider) { return ((IConvertible)this.ToDecimal(provider)).ToUInt32(provider); } ulong IConvertible.ToUInt64(IFormatProvider provider) { return ((IConvertible)this.ToDecimal(provider)).ToUInt64(provider); } DateTime IConvertible.ToDateTime(IFormatProvider provider) { return new DateTime(((IConvertible)this).ToInt64(provider)); } TypeCode IConvertible.GetTypeCode() { return this.numerator.GetTypeCode(); } object IConvertible.ToType(Type conversionType, IFormatProvider provider) { if (conversionType == null) { throw new ArgumentNullException("conversionType"); } Type thisType = this.GetType(); if (thisType == conversionType) { // no conversion needed return this; } if (!conversionType.IsGenericType || typeof(Rational<>) != conversionType.GetGenericTypeDefinition()) { // fall back to basic conversion return Convert.ChangeType(this, conversionType, provider); } // auto-convert between Rational types by converting Numerator/Denominator Type genericArg = conversionType.GetGenericArguments()[0]; object[] ctorArgs = { Convert.ChangeType(this.Numerator, genericArg, provider), Convert.ChangeType(this.Denominator, genericArg, provider) }; ConstructorInfo ctor = conversionType.GetConstructor(new Type[] { genericArg, genericArg }); if (ctor == null) { throw new InvalidCastException("Unable to find constructor for Rational<"+genericArg.Name+">."); } return ctor.Invoke(ctorArgs); } #endregion IConvertible Members #region IComparable Members /// /// Compares this instance to a specified System.Object. /// /// /// public int CompareTo(object that) { if (that is Rational) { // differentiate between a real zero and a divide by zero // work around divide by zero value to get meaningful comparisons Rational other = (Rational)that; if (Convert.ToDecimal(this.denominator) == Decimal.Zero) { if (Convert.ToDecimal(other.denominator) == Decimal.Zero) { return Convert.ToDecimal(this.numerator).CompareTo(Convert.ToDecimal(other.numerator)); } else if (Convert.ToDecimal(other.numerator) == Decimal.Zero) { return Convert.ToDecimal(this.denominator).CompareTo(Convert.ToDecimal(other.denominator)); } } else if (Convert.ToDecimal(other.denominator) == Decimal.Zero) { if (Convert.ToDecimal(this.numerator) == Decimal.Zero) { return Convert.ToDecimal(this.denominator).CompareTo(Convert.ToDecimal(other.denominator)); } } } return Convert.ToDecimal(this).CompareTo(Convert.ToDecimal(that)); } #endregion IComparable Members #region IComparable Members /// /// Compares this instance to another instance. /// /// /// public int CompareTo(T that) { return Decimal.Compare(Convert.ToDecimal(this), Convert.ToDecimal(that)); } #endregion IComparable Members #region Operators /// /// Negation /// /// /// public static Rational operator-(Rational r) { T numerator = (T)Convert.ChangeType(-Convert.ToDecimal(r.numerator), typeof(T)); return new Rational(numerator, r.denominator); } /// /// Addition /// /// /// /// public static Rational operator+(Rational r1, Rational r2) { decimal n1 = Convert.ToDecimal(r1.numerator); decimal d1 = Convert.ToDecimal(r1.denominator); decimal n2 = Convert.ToDecimal(r2.numerator); decimal d2 = Convert.ToDecimal(r2.denominator); decimal denominator = Rational.LCD(d1, d2); if (denominator > d1) { n1 *= (denominator/d1); } if (denominator > d2) { n2 *= (denominator/d2); } decimal numerator = n1 + n2; return new Rational((T)Convert.ChangeType(numerator, typeof(T)), (T)Convert.ChangeType(denominator, typeof(T))); } /// /// Subtraction /// /// /// /// public static Rational operator-(Rational r1, Rational r2) { return r1 + (-r2); } /// /// Multiplication /// /// /// /// public static Rational operator*(Rational r1, Rational r2) { decimal numerator = Convert.ToDecimal(r1.numerator) * Convert.ToDecimal(r2.numerator); decimal denominator = Convert.ToDecimal(r1.denominator) * Convert.ToDecimal(r2.denominator); return new Rational((T)Convert.ChangeType(numerator, typeof(T)), (T)Convert.ChangeType(denominator, typeof(T))); } /// /// Division /// /// /// /// public static Rational operator/(Rational r1, Rational r2) { return r1 * new Rational(r2.denominator, r2.numerator); } /// /// Less than /// /// /// /// public static bool operator<(Rational r1, Rational r2) { return r1.CompareTo(r2) < 0; } /// /// Less than or equal to /// /// /// /// public static bool operator<=(Rational r1, Rational r2) { return r1.CompareTo(r2) <= 0; } /// /// Greater than /// /// /// /// public static bool operator>(Rational r1, Rational r2) { return r1.CompareTo(r2) > 0; } /// /// Greater than or equal to /// /// /// /// public static bool operator>=(Rational r1, Rational r2) { return r1.CompareTo(r2) >= 0; } /// /// Equal to /// /// /// /// public static bool operator==(Rational r1, Rational r2) { return r1.CompareTo(r2) == 0; } /// /// Not equal to /// /// /// /// public static bool operator!=(Rational r1, Rational r2) { return r1.CompareTo(r2) != 0; } #endregion Operators #region Object Overrides public override string ToString() { return Convert.ToString(this); } public override bool Equals(object obj) { return (this.CompareTo(obj) == 0); } public override int GetHashCode() { // adapted from Anonymous Type: { uint Numerator, uint Denominator } int num = 0x1fb8d67d; num = (-1521134295 * num) + EqualityComparer.Default.GetHashCode(this.numerator); return ((-1521134295 * num) + EqualityComparer.Default.GetHashCode(this.denominator)); } #endregion Object Overrides } }