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
{
@@ -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);
}

View File

@@ -496,6 +496,7 @@
<Compile Include="Media\HwEncCheck.cs" />
<Compile Include="Media\TimestampUtils.cs" />
<Compile Include="MiscUtils\Benchmarker.cs" />
<Compile Include="MiscUtils\FractionHelper.cs" />
<Compile Include="MiscUtils\FrameRename.cs" />
<Compile Include="MiscUtils\ModelDownloadFormUtils.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;
}
}
}