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;
+ }
+ }
+}