Much better fraction approximation

This commit is contained in:
N00MKRAD
2025-03-02 21:14:45 +01:00
parent e20c4c922c
commit 2253ddae06
3 changed files with 115 additions and 20 deletions

View File

@@ -1,4 +1,5 @@
using System; using Flowframes.MiscUtils;
using System;
namespace Flowframes.Data namespace Flowframes.Data
{ {
@@ -6,21 +7,21 @@ namespace Flowframes.Data
{ {
public long Numerator = 0; public long Numerator = 0;
public long Denominator = 1; public long Denominator = 1;
public static Fraction Zero = new Fraction(0, 0); public static Fraction Zero = new Fraction(0, 1);
public Fraction() { } public Fraction() { }
public Fraction(long numerator, long denominator) public Fraction(long numerator, long denominator)
{ {
this.Numerator = numerator; Numerator = numerator;
this.Denominator = denominator; Denominator = denominator;
//If denominator negative... //If denominator negative...
if (this.Denominator < 0) if (Denominator < 0)
{ {
//...move the negative up to the numerator //...move the negative up to the numerator
this.Numerator = -this.Numerator; Numerator = -Numerator;
this.Denominator = -this.Denominator; Denominator = -Denominator;
} }
} }
@@ -32,11 +33,10 @@ namespace Flowframes.Data
public Fraction(float value) public Fraction(float value)
{ {
Numerator = (value * 10000f).RoundToInt(); int maxDigits = 4;
Denominator = 10000; var (num, den) = FractionHelper.FloatToApproxFraction(value, maxDigits);
var reducedFrac = GetReduced(); Numerator = num;
Numerator = reducedFrac.Numerator; Denominator = den;
Denominator = reducedFrac.Denominator;
} }
public Fraction(string text) public Fraction(string text)
@@ -129,16 +129,16 @@ namespace Flowframes.Data
Fraction modifiedFraction = this; Fraction modifiedFraction = this;
//Cannot reduce to smaller denominators //Cannot reduce to smaller denominators
if (targetDenominator < this.Denominator) if (targetDenominator < Denominator)
return modifiedFraction; return modifiedFraction;
//The target denominator must be a factor of the current denominator //The target denominator must be a factor of the current denominator
if (targetDenominator % this.Denominator != 0) if (targetDenominator % Denominator != 0)
return modifiedFraction; return modifiedFraction;
if (this.Denominator != targetDenominator) if (Denominator != targetDenominator)
{ {
long factor = targetDenominator / this.Denominator; long factor = targetDenominator / Denominator;
modifiedFraction.Denominator = targetDenominator; modifiedFraction.Denominator = targetDenominator;
modifiedFraction.Numerator *= factor; modifiedFraction.Numerator *= factor;
} }
@@ -165,8 +165,8 @@ namespace Flowframes.Data
//Make sure only a single negative sign is on the numerator //Make sure only a single negative sign is on the numerator
if (modifiedFraction.Denominator < 0) if (modifiedFraction.Denominator < 0)
{ {
modifiedFraction.Numerator = -this.Numerator; modifiedFraction.Numerator = -Numerator;
modifiedFraction.Denominator = -this.Denominator; modifiedFraction.Denominator = -Denominator;
} }
} }
catch (Exception e) catch (Exception e)
@@ -180,7 +180,7 @@ namespace Flowframes.Data
public Fraction GetReciprocal() public Fraction GetReciprocal()
{ {
//Flip the numerator and the denominator //Flip the numerator and the denominator
return new Fraction(this.Denominator, this.Numerator); return new Fraction(Denominator, Numerator);
} }

View File

@@ -496,6 +496,7 @@
<Compile Include="Media\HwEncCheck.cs" /> <Compile Include="Media\HwEncCheck.cs" />
<Compile Include="Media\TimestampUtils.cs" /> <Compile Include="Media\TimestampUtils.cs" />
<Compile Include="MiscUtils\Benchmarker.cs" /> <Compile Include="MiscUtils\Benchmarker.cs" />
<Compile Include="MiscUtils\FractionHelper.cs" />
<Compile Include="MiscUtils\FrameRename.cs" /> <Compile Include="MiscUtils\FrameRename.cs" />
<Compile Include="MiscUtils\ModelDownloadFormUtils.cs" /> <Compile Include="MiscUtils\ModelDownloadFormUtils.cs" />
<Compile Include="MiscUtils\NmkdStopwatch.cs" /> <Compile Include="MiscUtils\NmkdStopwatch.cs" />

View File

@@ -0,0 +1,94 @@
using System;
namespace Flowframes.MiscUtils
{
internal class FractionHelper
{
/// <summary>
/// Converts a float (<paramref name="value"/>) 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 (<paramref name="maxDigits"/>).
/// </summary>
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);
}
/// <summary>
/// Computes the greatest common divisor (Euclid's algorithm).
/// </summary>
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;
}
}
}