From f93dd1470ee7fa252ff23f70f3cd7c9c2a19307e Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 16 Aug 2016 15:15:23 +1000 Subject: [PATCH] Better Rational + Tests Former-commit-id: f062d6e9e05b6f71bc8a5072059956c425ea8bbc Former-commit-id: 51371aecc20f3985e50590491228ae1538847a09 Former-commit-id: cbbb41ffa048ce953e297c0a2b32de269bf83572 --- src/ImageProcessorCore/Numerics/Rational.cs | 159 +++++++++++++----- .../Numerics/RationalTests.cs | 57 +++++++ 2 files changed, 176 insertions(+), 40 deletions(-) create mode 100644 tests/ImageProcessorCore.Tests/Numerics/RationalTests.cs diff --git a/src/ImageProcessorCore/Numerics/Rational.cs b/src/ImageProcessorCore/Numerics/Rational.cs index 0b66d8367..090af6e3c 100644 --- a/src/ImageProcessorCore/Numerics/Rational.cs +++ b/src/ImageProcessorCore/Numerics/Rational.cs @@ -19,29 +19,49 @@ namespace ImageProcessorCore public struct Rational : IEquatable { /// - /// Represents a rational object that is not a number. NaN + /// Represents a rational object that is not a number; NaN (0, 0) /// - public static Rational Indeterminate = new Rational(3, 0); + public static Rational Indeterminate = new Rational(BigInteger.Zero); /// - /// Represents a rational object that is equal to 0. + /// Represents a rational object that is equal to 0 (0, 1) /// - public static Rational Zero = new Rational(0, 0); + public static Rational Zero = new Rational(BigInteger.Zero); /// - /// Represents a rational object that is equal to 1. + /// Represents a rational object that is equal to 1 (1, 1) /// - public static Rational One = new Rational(1, 1); + public static Rational One = new Rational(BigInteger.One); /// - /// Represents a Rational object that is equal to negative infinity (-1, 0). + /// Represents a Rational object that is equal to negative infinity (-1, 0) /// - public static readonly Rational NegativeInfinity = new Rational(-1, 0); + public static readonly Rational NegativeInfinity = new Rational(BigInteger.MinusOne, BigInteger.Zero); /// - /// Represents a Rational object that is equal to positive infinity (+1, 0). + /// Represents a Rational object that is equal to positive infinity (1, 0) /// - public static readonly Rational PositiveInfinity = new Rational(1, 0); + public static readonly Rational PositiveInfinity = new Rational(BigInteger.One, BigInteger.Zero); + + /// + /// The maximum number of decimal places + /// + private const int DoubleMaxScale = 308; + + /// + /// The maximum precision (numbers after the decimal point) + /// + private static readonly BigInteger DoublePrecision = BigInteger.Pow(10, DoubleMaxScale); + + /// + /// Represents double.MaxValue + /// + private static readonly BigInteger DoubleMaxValue = (BigInteger)double.MaxValue; + + /// + /// Represents double.MinValue + /// + private static readonly BigInteger DoubleMinValue = (BigInteger)double.MinValue; /// /// Initializes a new instance of the struct. @@ -159,81 +179,81 @@ namespace ImageProcessorCore { get { - if (this.Denominator != 0) + if (this.Denominator != BigInteger.Zero) { return false; } - return this.Numerator == 3; + return this.Numerator == BigInteger.Zero; } } /// - /// Gets a value indicating whether this instance is an integer. + /// Gets a value indicating whether this instance is an integer (n, 1) /// - public bool IsInteger => this.Denominator == 1; + public bool IsInteger => this.Denominator == BigInteger.One; /// - /// Gets a value indicating whether this instance is equal to 0 + /// Gets a value indicating whether this instance is equal to 0 (0, 1) /// public bool IsZero { get { - if (this.Denominator != 0) + if (this.Denominator != BigInteger.One) { return false; } - return this.Numerator == 0; + return this.Numerator == BigInteger.Zero; } } /// - /// Gets a value indicating whether this instance is equal to 1. + /// Gets a value indicating whether this instance is equal to 1 (1, 1) /// public bool IsOne { get { - if (this.Denominator != 1) + if (this.Denominator != BigInteger.One) { return false; } - return this.Numerator == 1; + return this.Numerator == BigInteger.One; } } /// - /// Gets a value indicating whether this instance is equal to negative infinity (-1, 0). + /// Gets a value indicating whether this instance is equal to negative infinity (-1, 0) /// public bool IsNegativeInfinity { get { - if (this.Denominator != 0) + if (this.Denominator != BigInteger.Zero) { return false; } - return this.Numerator == -1; + return this.Numerator == BigInteger.MinusOne; } } /// - /// Gets a value indicating whether this instance is equal to positive infinity (+1, 0). + /// Gets a value indicating whether this instance is equal to positive infinity (1, 0) /// public bool IsPositiveInfinity { get { - if (this.Denominator != 0) + if (this.Denominator != BigInteger.Zero) { return false; } - return this.Numerator == 1; + return this.Numerator == BigInteger.One; } } @@ -245,6 +265,7 @@ namespace ImageProcessorCore /// public double ToDouble() { + // Shortcut return values if (this.IsIndeterminate) { return double.NaN; @@ -265,7 +286,54 @@ namespace ImageProcessorCore return (double)this.Numerator; } - return (double)(this.Numerator / this.Denominator); + // The Double value type represents a double-precision 64-bit number with + // values ranging from -1.79769313486232e308 to +1.79769313486232e308 + // values that do not fit into this range are returned as +/-Infinity + if (SafeCastToDouble(this.Numerator) && SafeCastToDouble(this.Denominator)) + { + return (double)this.Numerator / (double)this.Denominator; + } + + // Scale the numerator to preserve the fraction part through the integer division + // We could probably adjust this to make it less precise if need be. + BigInteger denormalized = (this.Numerator * DoublePrecision) / this.Denominator; + if (denormalized.IsZero) + { + // underflow to -+0 + return (this.Numerator.Sign < 0) ? BitConverter.Int64BitsToDouble(unchecked((long)0x8000000000000000)) : 0d; + } + + double result = 0; + bool isDouble = false; + int scale = DoubleMaxScale; + + while (scale > 0) + { + if (!isDouble) + { + if (SafeCastToDouble(denormalized)) + { + result = (double)denormalized; + isDouble = true; + } + else + { + denormalized = denormalized / 10; + } + } + + result = result / 10; + scale--; + } + + if (!isDouble) + { + return (this.Numerator.Sign < 0) ? double.NegativeInfinity : double.PositiveInfinity; + } + else + { + return result; + } } /// @@ -289,14 +357,14 @@ namespace ImageProcessorCore } // Indeterminate - if (this.Numerator == 3 && this.Denominator == 0) + if (this.Numerator == BigInteger.Zero && this.Denominator == BigInteger.Zero) { - return other.Numerator == 3 && other.Denominator == 0; + return other.Numerator == BigInteger.Zero && other.Denominator == BigInteger.Zero; } - if (other.Numerator == 3 && other.Denominator == 0) + if (other.Numerator == BigInteger.Zero && other.Denominator == BigInteger.Zero) { - return this.Numerator == 3 && this.Denominator == 0; + return this.Numerator == BigInteger.Zero && this.Denominator == BigInteger.Zero; } // ad = bc @@ -342,7 +410,7 @@ namespace ImageProcessorCore if (this.IsZero) { - return "[ Zero ]"; + return "0"; } if (this.IsInteger) @@ -382,16 +450,16 @@ namespace ImageProcessorCore return; } - if (this.Numerator == 0) + if (this.Numerator == BigInteger.Zero) { - this.Denominator = 1; + this.Denominator = BigInteger.One; return; } if (this.Numerator == this.Denominator) { - this.Numerator = 1; - this.Denominator = 1; + this.Numerator = BigInteger.One; + this.Denominator = BigInteger.One; return; } @@ -414,20 +482,20 @@ namespace ImageProcessorCore int eIndex = value.IndexOf("E", StringComparison.Ordinal); int slashIndex = value.IndexOf("/", StringComparison.Ordinal); - // An integer such as 7 + // An integer such as 3 if (periodIndex == -1 && eIndex == -1 && slashIndex == -1) { return new Rational(BigInteger.Parse(value)); } - // A fraction such as 3/7 + // A fraction such as 22/7 if (periodIndex == -1 && eIndex == -1 && slashIndex != -1) { return new Rational(BigInteger.Parse(value.Substring(0, slashIndex)), BigInteger.Parse(value.Substring(slashIndex + 1))); } - // No scientific Notation such as 5.997 + // No scientific Notation such as 3.14159 if (eIndex == -1) { BigInteger n = BigInteger.Parse(value.Replace(".", string.Empty)); @@ -435,7 +503,7 @@ namespace ImageProcessorCore return new Rational(n, d); } - // Scientific notation such as 2.4556E-2 + // Scientific notation such as 3.14159E-2 int characteristic = int.Parse(value.Substring(eIndex + 1)); BigInteger ten = 10; BigInteger numerator = BigInteger.Parse(value.Substring(0, eIndex).Replace(".", string.Empty)); @@ -454,6 +522,17 @@ namespace ImageProcessorCore return new Rational(numerator, denominator); } + /// + /// Returns a value indicating whether the given big integer can be + /// safely cast to a double. + /// + /// The value to test. + /// true if the value can be safely cast + private static bool SafeCastToDouble(BigInteger value) + { + return DoubleMinValue <= value && value <= DoubleMaxValue; + } + /// /// Returns the hash code for this instance. /// diff --git a/tests/ImageProcessorCore.Tests/Numerics/RationalTests.cs b/tests/ImageProcessorCore.Tests/Numerics/RationalTests.cs new file mode 100644 index 000000000..246349f82 --- /dev/null +++ b/tests/ImageProcessorCore.Tests/Numerics/RationalTests.cs @@ -0,0 +1,57 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageProcessorCore.Tests +{ + using Xunit; + + /// + /// Tests the struct. + /// + public class RationalTests + { + /// + /// Tests the equality operators for equality. + /// + [Fact] + public void AreEqual() + { + Rational r1 = new Rational(3, 2); + Rational r2 = new Rational(3, 2); + + Assert.Equal(r1, r2); + + Rational r3 = new Rational(7.55); + Rational r4 = new Rational(755, 100); + Rational r5 = new Rational(151, 20); + + Assert.Equal(r3, r4); + Assert.Equal(r4, r5); + } + + /// + /// Tests the equality operators for inequality. + /// + [Fact] + public void AreNotEqual() + { + Rational first = new Rational(0, 100); + Rational second = new Rational(100, 100); + + Assert.NotEqual(first, second); + } + + /// + /// Tests whether the Rational constructor correctly assign properties. + /// + [Fact] + public void ConstructorAssignsProperties() + { + Rational first = new Rational(4, 5); + Assert.Equal(4, first.Numerator); + Assert.Equal(5, first.Denominator); + } + } +} \ No newline at end of file