From 2253ddae064c80b08a5561a5f2ef36dc7301b6de Mon Sep 17 00:00:00 2001 From: N00MKRAD <61149547+n00mkrad@users.noreply.github.com> Date: Sun, 2 Mar 2025 21:14:45 +0100 Subject: [PATCH] Much better fraction approximation --- CodeLegacy/Data/Fraction.cs | 40 +++++------ CodeLegacy/Flowframes.csproj | 1 + CodeLegacy/MiscUtils/FractionHelper.cs | 94 ++++++++++++++++++++++++++ 3 files changed, 115 insertions(+), 20 deletions(-) create mode 100644 CodeLegacy/MiscUtils/FractionHelper.cs diff --git a/CodeLegacy/Data/Fraction.cs b/CodeLegacy/Data/Fraction.cs index 9af1409..d3481b0 100644 --- a/CodeLegacy/Data/Fraction.cs +++ b/CodeLegacy/Data/Fraction.cs @@ -1,4 +1,5 @@ -using System; +using Flowframes.MiscUtils; +using System; namespace Flowframes.Data { @@ -6,21 +7,21 @@ namespace Flowframes.Data { public long Numerator = 0; public long Denominator = 1; - public static Fraction Zero = new Fraction(0, 0); + public static Fraction Zero = new Fraction(0, 1); public Fraction() { } public Fraction(long numerator, long denominator) { - this.Numerator = numerator; - this.Denominator = denominator; + Numerator = numerator; + Denominator = denominator; //If denominator negative... - if (this.Denominator < 0) + if (Denominator < 0) { //...move the negative up to the numerator - this.Numerator = -this.Numerator; - this.Denominator = -this.Denominator; + Numerator = -Numerator; + Denominator = -Denominator; } } @@ -32,11 +33,10 @@ namespace Flowframes.Data public Fraction(float value) { - Numerator = (value * 10000f).RoundToInt(); - Denominator = 10000; - var reducedFrac = GetReduced(); - Numerator = reducedFrac.Numerator; - Denominator = reducedFrac.Denominator; + int maxDigits = 4; + var (num, den) = FractionHelper.FloatToApproxFraction(value, maxDigits); + Numerator = num; + Denominator = den; } public Fraction(string text) @@ -72,7 +72,7 @@ namespace Flowframes.Data Numerator = floatFrac.Numerator; Denominator = floatFrac.Denominator; } - + return; } @@ -129,16 +129,16 @@ namespace Flowframes.Data Fraction modifiedFraction = this; //Cannot reduce to smaller denominators - if (targetDenominator < this.Denominator) + if (targetDenominator < Denominator) return modifiedFraction; //The target denominator must be a factor of the current denominator - if (targetDenominator % this.Denominator != 0) + if (targetDenominator % Denominator != 0) return modifiedFraction; - if (this.Denominator != targetDenominator) + if (Denominator != targetDenominator) { - long factor = targetDenominator / this.Denominator; + long factor = targetDenominator / Denominator; modifiedFraction.Denominator = targetDenominator; modifiedFraction.Numerator *= factor; } @@ -165,8 +165,8 @@ namespace Flowframes.Data //Make sure only a single negative sign is on the numerator if (modifiedFraction.Denominator < 0) { - modifiedFraction.Numerator = -this.Numerator; - modifiedFraction.Denominator = -this.Denominator; + modifiedFraction.Numerator = -Numerator; + modifiedFraction.Denominator = -Denominator; } } catch (Exception e) @@ -180,7 +180,7 @@ namespace Flowframes.Data public Fraction GetReciprocal() { //Flip the numerator and the denominator - return new Fraction(this.Denominator, this.Numerator); + return new Fraction(Denominator, Numerator); } diff --git a/CodeLegacy/Flowframes.csproj b/CodeLegacy/Flowframes.csproj index 08b6874..7476f2c 100644 --- a/CodeLegacy/Flowframes.csproj +++ b/CodeLegacy/Flowframes.csproj @@ -496,6 +496,7 @@ + diff --git a/CodeLegacy/MiscUtils/FractionHelper.cs b/CodeLegacy/MiscUtils/FractionHelper.cs new file mode 100644 index 0000000..cdccc41 --- /dev/null +++ b/CodeLegacy/MiscUtils/FractionHelper.cs @@ -0,0 +1,94 @@ +using System; + +namespace Flowframes.MiscUtils +{ + internal class FractionHelper + { + /// + /// Converts a float () to an approximated fraction that is as close to the original value as possible, with a limit on the number of digits for numerator and denominator (). + /// + public static (int Numerator, int Denominator) FloatToApproxFraction(float value, int maxDigits = 4) + { + if (float.IsNaN(value) || float.IsInfinity(value)) + throw new ArgumentException("Value must be a finite float."); + + // Special case: zero + if (Math.Abs(value) < float.Epsilon) + return (0, 1); + + // Determine the sign and work with absolute value for searching. + int sign = Math.Sign(value); + double target = Math.Abs((double)value); + + // Upper bound for numerator/denominator based on max digits + // e.g. if maxDigits = 4, limit = 9999 + int limit = (int)Math.Pow(10, maxDigits) - 1; + + // We'll track the best fraction found + double bestError = double.MaxValue; + int bestNum = 0; + int bestDen = 1; + + // Simple brute-force search over all possible denominators + for (int d = 1; d <= limit; d++) + { + // Round the numerator for the current denominator + int n = (int)Math.Round(target * d); + + // If n is 0, skip (except the value might be < 0.5/d, but continue searching) + if (n == 0) + continue; + + // If the numerator exceeds the limit, skip + if (n > limit) + continue; + + // Evaluate how close n/d is to the target + double fractionValue = (double)n / d; + double error = Math.Abs(fractionValue - target); + + // If it's closer, record it as our best + if (error < bestError) + { + bestError = error; + bestNum = n; + bestDen = d; + } + } + + // Reapply the sign to the numerator + bestNum *= sign; + + // Reduce fraction by GCD (to get simplest form) + int gcd = GCD(bestNum, bestDen); + bestNum /= gcd; + bestDen /= gcd; + + // If the denominator is 1 after reduction, just return the integer + if (bestDen == 1) + { + return (bestNum, 1); + } + + // Otherwise return "numerator/denominator" + Logger.Log($"Approximated fraction for {value}: {bestNum}/{bestDen} (={((float)bestNum / bestDen).ToString("0.0#######")})", true); + return (bestNum, bestDen); + } + + /// + /// Computes the greatest common divisor (Euclid's algorithm). + /// + private static int GCD(int a, int b) + { + a = Math.Abs(a); + b = Math.Abs(b); + while (b != 0) + { + int t = b; + b = a % b; + a = t; + } + return a; + } + } +}