2025-03-02 21:14:45 +01:00
using Flowframes.MiscUtils ;
using System ;
2021-08-23 16:50:18 +02:00
namespace Flowframes.Data
{
2024-11-28 16:08:04 +01:00
public class Fraction
2021-08-23 16:50:18 +02:00
{
2024-11-28 16:08:04 +01:00
public long Numerator = 0 ;
public long Denominator = 1 ;
2025-03-02 21:14:45 +01:00
public static Fraction Zero = new Fraction ( 0 , 1 ) ;
2021-08-23 16:50:18 +02:00
2024-11-28 16:08:04 +01:00
public Fraction ( ) { }
2021-09-28 18:27:22 +02:00
public Fraction ( long numerator , long denominator )
2021-08-23 16:50:18 +02:00
{
2025-03-02 21:14:45 +01:00
Numerator = numerator ;
Denominator = denominator ;
2021-08-23 16:50:18 +02:00
//If denominator negative...
2025-03-02 21:14:45 +01:00
if ( Denominator < 0 )
2021-08-23 16:50:18 +02:00
{
//...move the negative up to the numerator
2025-03-02 21:14:45 +01:00
Numerator = - Numerator ;
Denominator = - Denominator ;
2021-08-23 16:50:18 +02:00
}
}
public Fraction ( Fraction fraction )
{
Numerator = fraction . Numerator ;
Denominator = fraction . Denominator ;
}
2025-03-10 11:58:47 +01:00
/// <summary>
/// Initializes a new Fraction by approximating the <paramref name="value"/> as a fraction using up to 4 digits.
/// </summary>
2021-08-23 16:50:18 +02:00
public Fraction ( float value )
{
2025-03-02 21:14:45 +01:00
int maxDigits = 4 ;
var ( num , den ) = FractionHelper . FloatToApproxFraction ( value , maxDigits ) ;
Numerator = num ;
Denominator = den ;
2021-08-23 16:50:18 +02:00
}
2025-03-10 11:58:47 +01:00
/// <summary>
/// Initializes a new Fraction from a string <paramref name="text"/>. If the text represents a single number or a fraction, it parses accordingly.
/// </summary>
2021-08-23 16:50:18 +02:00
public Fraction ( string text )
{
try
{
2024-08-23 16:11:38 +02:00
if ( text . IsEmpty ( ) )
{
Numerator = 0 ;
Denominator = 1 ;
return ;
}
2024-11-08 11:54:26 +01:00
text = text . Replace ( ':' , '/' ) ; // Replace colon with slash in case someone thinks it's a good idea to write a fraction like that
2021-08-23 16:50:18 +02:00
string [ ] numbers = text . Split ( '/' ) ;
2024-08-21 13:27:01 +02:00
2024-11-08 11:54:26 +01:00
// If split is only 1 item, it's a single number, not a fraction
2024-08-21 13:27:01 +02:00
if ( numbers . Length = = 1 )
{
2024-11-08 11:54:26 +01:00
float numFloat = numbers [ 0 ] . GetFloat ( ) ;
int numInt = numFloat . RoundToInt ( ) ;
// If parsed float is equal to the rounded int, it's a whole number
if ( numbers [ 0 ] . GetFloat ( ) . EqualsRoughly ( numInt ) )
{
Numerator = numInt ;
Denominator = 1 ;
}
else
{
// Use float constructor if not a whole number
2024-11-28 16:08:04 +01:00
var floatFrac = new Fraction ( numFloat ) ;
Numerator = floatFrac . Numerator ;
Denominator = floatFrac . Denominator ;
2024-11-08 11:54:26 +01:00
}
2025-03-02 21:14:45 +01:00
2024-08-21 13:27:01 +02:00
return ;
}
Numerator = numbers [ 0 ] . GetFloat ( ) . RoundToInt ( ) ;
2021-08-23 16:50:18 +02:00
Denominator = numbers [ 1 ] . GetInt ( ) ;
}
catch
{
try
{
Numerator = text . GetFloat ( ) . RoundToInt ( ) ;
Denominator = 1 ;
}
catch
{
Numerator = 0 ;
2024-08-21 13:27:01 +02:00
Denominator = 1 ;
2021-08-23 16:50:18 +02:00
}
}
}
2025-03-10 11:58:47 +01:00
/// <summary>
/// Calculates and returns the greatest common denominator (GCD) for <paramref name="a"/> and <paramref name="b"/> by dropping negative signs and using the modulo operation.
/// </summary>
2021-09-28 18:27:22 +02:00
private static long GetGreatestCommonDenominator ( long a , long b )
2021-08-23 16:50:18 +02:00
{
//Drop negative signs
a = Math . Abs ( a ) ;
b = Math . Abs ( b ) ;
2025-03-10 11:58:47 +01:00
//Return the greatest common denominator between two longs
2021-08-23 16:50:18 +02:00
while ( a ! = 0 & & b ! = 0 )
{
if ( a > b )
a % = b ;
else
b % = a ;
}
if ( a = = 0 )
return b ;
else
return a ;
}
2025-03-10 11:58:47 +01:00
/// <summary>
/// Calculates and returns the least common denominator for <paramref name="a"/> and <paramref name="b"/> using their greatest common denominator.
/// </summary>
2021-09-28 18:27:22 +02:00
private static long GetLeastCommonDenominator ( long a , long b )
2021-08-23 16:50:18 +02:00
{
2021-09-28 18:27:22 +02:00
return ( a * b ) / GetGreatestCommonDenominator ( a , b ) ;
2021-08-23 16:50:18 +02:00
}
2025-03-10 11:58:47 +01:00
/// <summary>
/// Converts the fraction to have the specified <paramref name="targetDenominator"/> if possible by scaling the numerator accordingly; returns a Fraction with the target denominator or the current fraction if conversion is not possible.
/// </summary>
2021-09-28 18:27:22 +02:00
public Fraction ToDenominator ( long targetDenominator )
2021-08-23 16:50:18 +02:00
{
Fraction modifiedFraction = this ;
2025-03-10 11:58:47 +01:00
// Cannot reduce to smaller denominators & target denominator must be a factor of the current denominator
if ( targetDenominator < Denominator | | targetDenominator % Denominator ! = 0 )
2021-08-23 16:50:18 +02:00
return modifiedFraction ;
2025-03-02 21:14:45 +01:00
if ( Denominator ! = targetDenominator )
2021-08-23 16:50:18 +02:00
{
2025-03-10 11:58:47 +01:00
long factor = targetDenominator / Denominator ; // Find factor to multiply the fraction by to make the denominator match the target denominator
2021-08-23 16:50:18 +02:00
modifiedFraction . Denominator = targetDenominator ;
modifiedFraction . Numerator * = factor ;
}
return modifiedFraction ;
}
2025-03-10 11:58:47 +01:00
/// <summary>
/// Reduces the fraction to its lowest terms by repeatedly dividing the numerator and denominator by their greatest common denominator.
/// </summary>
2021-08-23 16:50:18 +02:00
public Fraction GetReduced ( )
{
Fraction modifiedFraction = this ;
try
{
2025-03-10 11:58:47 +01:00
//While the numerator and denominator share a greatest common denominator, keep dividing both by it
2021-09-28 18:27:22 +02:00
long gcd = 0 ;
while ( Math . Abs ( gcd = GetGreatestCommonDenominator ( modifiedFraction . Numerator , modifiedFraction . Denominator ) ) ! = 1 )
2021-08-23 16:50:18 +02:00
{
modifiedFraction . Numerator / = gcd ;
modifiedFraction . Denominator / = gcd ;
}
//Make sure only a single negative sign is on the numerator
if ( modifiedFraction . Denominator < 0 )
{
2025-03-02 21:14:45 +01:00
modifiedFraction . Numerator = - Numerator ;
modifiedFraction . Denominator = - Denominator ;
2021-08-23 16:50:18 +02:00
}
}
catch ( Exception e )
{
Logger . Log ( $"Failed to reduce fraction ({modifiedFraction}): {e.Message}" , true ) ;
}
return modifiedFraction ;
}
2025-03-10 11:58:47 +01:00
/// <summary>
/// Returns a new Fraction that is the reciprocal of the current fraction by swapping the numerator and denominator.
/// </summary>
2021-08-23 16:50:18 +02:00
public Fraction GetReciprocal ( )
{
2025-03-02 21:14:45 +01:00
return new Fraction ( Denominator , Numerator ) ;
2021-08-23 16:50:18 +02:00
}
2025-03-10 11:58:47 +01:00
/// <summary>
/// Combines two fractions <paramref name="f1"/> and <paramref name="f2"/> using the specified <paramref name="combine"/> function after converting them to a common denominator; returns the reduced combined Fraction.
/// </summary>
private static Fraction Combine ( Fraction f1 , Fraction f2 , Func < long , long , long > combine )
2021-08-23 16:50:18 +02:00
{
2025-03-10 11:58:47 +01:00
if ( f1 . Denominator = = 0 )
return f2 ;
if ( f2 . Denominator = = 0 )
return f1 ;
2021-08-23 16:50:18 +02:00
2025-03-10 11:58:47 +01:00
long lcd = GetLeastCommonDenominator ( f1 . Denominator , f2 . Denominator ) ;
f1 = f1 . ToDenominator ( lcd ) ;
f2 = f2 . ToDenominator ( lcd ) ;
return new Fraction ( combine ( f1 . Numerator , f2 . Numerator ) , lcd ) . GetReduced ( ) ;
2021-08-23 16:50:18 +02:00
}
public override string ToString ( )
{
2024-10-13 16:58:06 +02:00
return $"{Numerator}/{Denominator}" ;
2021-08-23 16:50:18 +02:00
}
2025-03-10 11:58:47 +01:00
// Conversion properties
2024-10-13 16:58:06 +02:00
public float Float = > Denominator < 1 ? 0f : ( float ) Numerator / ( float ) Denominator ;
2025-03-10 11:58:47 +01:00
public double Double = > ( double ) Numerator / Denominator ;
2024-10-13 16:58:06 +02:00
public long Long = > Denominator < 1 ? 0L : ( long ) Numerator / ( long ) Denominator ;
2021-08-23 16:50:18 +02:00
2025-03-10 11:58:47 +01:00
// Operators
public static bool operator > ( Fraction frac , float value ) = > frac . Double > value ;
public static bool operator < ( Fraction frac , float value ) = > frac . Double < value ;
public static bool operator > ( float value , Fraction frac ) = > value > frac . Double ;
public static bool operator < ( float value , Fraction frac ) = > value < frac . Double ;
public static Fraction operator + ( Fraction frac1 , Fraction frac2 ) = > Combine ( frac1 , frac2 , ( a , b ) = > a + b ) ;
public static Fraction operator - ( Fraction frac1 , Fraction frac2 ) = > Combine ( frac1 , frac2 , ( a , b ) = > a - b ) ;
public static Fraction operator * ( Fraction frac1 , Fraction frac2 ) = > new Fraction ( frac1 . Numerator * frac2 . Numerator , frac1 . Denominator * frac2 . Denominator ) . GetReduced ( ) ;
public static Fraction operator / ( Fraction frac1 , Fraction frac2 ) = > new Fraction ( frac1 * frac2 . GetReciprocal ( ) ) . GetReduced ( ) ;
public static Fraction operator * ( Fraction frac , long mult ) = > new Fraction ( frac . Numerator * mult , frac . Denominator ) . GetReduced ( ) ;
public static Fraction operator * ( Fraction frac , double mult ) = > new Fraction ( ( long ) Math . Round ( frac . Numerator * mult ) , frac . Denominator ) . GetReduced ( ) ;
public static Fraction operator * ( Fraction frac , float mult ) = > new Fraction ( ( frac . Numerator * mult ) . RoundToInt ( ) , frac . Denominator ) . GetReduced ( ) ;
2024-10-13 16:58:06 +02:00
public string GetString ( string format = "0.#####" )
2021-08-23 16:50:18 +02:00
{
2024-10-13 16:58:06 +02:00
return ( ( float ) Numerator / Denominator ) . ToString ( format ) ;
2021-08-23 16:50:18 +02:00
}
}
}