opensheetmusicdisplay/opensheetmusicdisplay

View on GitHub
src/Common/DataObjects/Fraction.ts

Summary

Maintainability
F
3 days
Test Coverage
// TODO: Check the operators' names
// TODO: This class should probably be immutable?

/**
 * A class representing mathematical fractions, which have a numerator and a denominator.
 */
export class Fraction {
  private static maximumAllowedNumber: number = 46340; // sqrt(int.Max) --> signed int with 4 bytes (2^31)
  private numerator: number = 0;
  private denominator: number = 1;
  private wholeValue: number = 0;
  private realValue: number;

  /**
   * Returns the maximum of two fractions (does not clone)
   * @param f1
   * @param f2
   * @returns {Fraction}
   */
  public static max(f1: Fraction, f2: Fraction): Fraction {
    if (f1.RealValue > f2.RealValue) {
      return f1;
    } else {
      return f2;
    }
  }

  public static Equal(f1: Fraction, f2: Fraction): boolean {
    return f1.wholeValue === f2.wholeValue && f1.Denominator === f2.Denominator && f1.Numerator === f2.Numerator;
  }

  /**
   * The same as Fraction.clone
   * @param fraction
   * @returns {Fraction}
   */
  public static createFromFraction(fraction: Fraction): Fraction {
    return new Fraction(fraction.numerator, fraction.denominator, fraction.wholeValue, false);
  }

  public static plus(f1: Fraction, f2: Fraction): Fraction {
    const sum: Fraction = f1.clone();
    sum.Add(f2);
    return sum;
  }

  public static minus(f1: Fraction, f2: Fraction): Fraction {
    const sum: Fraction = f1.clone();
    sum.Sub(f2);
    return sum;
  }

    public static multiply (f1: Fraction, f2: Fraction): Fraction {
        return new Fraction ( (f1.wholeValue * f1.denominator + f1.numerator) * (f2.wholeValue * f2.denominator + f2.numerator),
                              f1.denominator * f2.denominator);
    }

  private static greatestCommonDenominator(a: number, b: number): number {
    if (a === 0) {
      return b;
    }

    if (b === 1) {
      return 1;
    }

    while (Math.abs(b) >= 1 && Math.abs(a) >= 1) { // accounts for floating point inaccuracies. smallest GCD is 1.
      // if we don't check a > 1e-8, we infinite loop for e.g. a = 2.666666666666667, b = 4. See #1478 (rare)
      if (a > b) {
        a -= b;
      } else {
        b -= a;
      }
    }

    const result: number = Math.round(a); // prevent returning 4.000001 or something, though it doesn't happen for our samples
    return Math.max(result, 1);
    // return 1 instead of 0, which causes NaNs e.g. in GraphicalMusicSheet.GetInterpolatedIndexInVerticalContainers()
    //   (though this case is rare anyways -> complex rhythms, see osmd #1511)
  }

  /**
   *
   * @param numerator
   * @param denominator
   * @param wholeValue - the integer number, needed for values greater than 1
   * @param simplify - If simplify is true, then the fraction is simplified
   * to make both the numerator and denominator coprime, and less than maximumAllowedNumber.
   */
  constructor(numerator: number = 0, denominator: number = 1, wholeValue: number = 0, simplify: boolean = true) {
    this.numerator = numerator;
    this.denominator = denominator;
    this.wholeValue = wholeValue;

    if (simplify) {
      this.simplify();
    }
    this.setRealValue();
  }

  public toString(): string {
    let result: string = this.numerator + "/" + this.denominator;
    if (this.wholeValue !== 0) {
      result = this.wholeValue + " " + result;
    }

    return result;
  }

  public clone(): Fraction {
    return new Fraction(this.numerator, this.denominator, this.wholeValue, false);
  }

  public get Numerator(): number {
    return this.numerator;
  }

  public set Numerator(value: number) {
    if (this.numerator !== value) {
      this.numerator = value;
      this.simplify();
      this.setRealValue();
    }
  }

  public get Denominator(): number {
    return this.denominator;
  }

  public set Denominator(value: number) {
    if (this.denominator !== value) {
      this.denominator = value;
      // don't simplify in case of a GraceNote (need it in order to set the right symbol)
      if (this.numerator !== 0) {
        this.simplify();
      }
      this.setRealValue();
    }
  }

  public get WholeValue(): number {
    return this.wholeValue;
  }

  public set WholeValue(value: number) {
    if (this.wholeValue !== value) {
      this.wholeValue = value;
      this.setRealValue();
    }
  }

  /**
   * Returns the unified numerator where the whole value will be expanded
   * with the denominator and added to the existing numerator.
   */
  public GetExpandedNumerator(): number {
    return this.wholeValue * this.denominator + this.numerator;
  }

  public calculateNumberOfNeededDots(): number {
    let num: number = 1;
    let product: number = 2;
    const expandedNumerator: number = this.GetExpandedNumerator();
    while (product < expandedNumerator) {
      num++;
      product = Math.pow(2, num);
    }
    return Math.min(3, num - 1);
  }

  public IsNegative(): boolean {
    return this.realValue < 0;
  }

  public get RealValue(): number {
    return this.realValue;
  }

  public expand(expansionValue: number): void {
    this.numerator *= expansionValue;
    this.denominator *= expansionValue;
    if (this.wholeValue !== 0) {
      this.numerator += this.wholeValue * this.denominator;
      this.wholeValue = 0;
    }
  }

  // public multiplyDenominatorWithFactor(factor: number): void {
  //   this.denominator *= factor;
  //   this.setRealValue();
  // }

  /**
   * Adds a Fraction to this Fraction.
   * Attention: This changes the already existing Fraction, which might be referenced elsewhere!
   * Use Fraction.plus() for creating a new Fraction object being the sum of two Fractions.
   * @param fraction the Fraction to add.
   */
  public Add(fraction: Fraction): Fraction {
    // normally should check if denominator or fraction.denominator is 0 but in our case
    // a zero denominator doesn't make sense
    this.numerator = (this.wholeValue * this.denominator + this.numerator) * fraction.denominator +
      (fraction.wholeValue * fraction.denominator + fraction.numerator) * this.denominator;
    this.denominator = this.denominator * fraction.denominator;
    this.wholeValue = 0;
    this.simplify();
    this.setRealValue();
    return this;
  }

  /**
   * Subtracts a Fraction from this Fraction.
   * Attention: This changes the already existing Fraction, which might be referenced elsewhere!
   * Use Fraction.minus() for creating a new Fraction object being the difference of two Fractions.
   * @param fraction the Fraction to subtract.
   */
  public Sub(fraction: Fraction): Fraction {
    // normally should check if denominator or fraction.denominator is 0 but in our case
    // a zero denominator doesn't make sense
    this.numerator = (this.wholeValue * this.denominator + this.numerator) * fraction.denominator -
      (fraction.wholeValue * fraction.denominator + fraction.numerator) * this.denominator;
    this.denominator = this.denominator * fraction.denominator;
    this.wholeValue = 0;
    this.simplify();
    this.setRealValue();
    return this;
  }
  /**
   * Brute Force quanization by searching incremental with the numerator until the denominator is
   * smaller/equal than the desired one.
   * @param maxAllowedDenominator
   */
  public Quantize(maxAllowedDenominator: number): Fraction {
    if (this.denominator <= maxAllowedDenominator) {
      return this;
    }

    const upTestFraction: Fraction = new Fraction(this.numerator + 1, this.denominator, this.wholeValue);

    while (upTestFraction.Denominator > maxAllowedDenominator) {
      upTestFraction.Numerator++;
    }

    if (this.numerator > this.denominator) {
      const downTestFraction: Fraction = new Fraction(this.numerator - 1, this.denominator, this.wholeValue);

      while (downTestFraction.Denominator > maxAllowedDenominator) {
        downTestFraction.Numerator--;
      }

      if (downTestFraction.Denominator < upTestFraction.Denominator) {
        return downTestFraction;
      }
    }
    return upTestFraction;
  }

  public Equals(obj: Fraction): boolean {
    return this.realValue === obj?.realValue;
  }

  public CompareTo(obj: Fraction): number {
    const diff: number = this.realValue - obj.realValue;
    // Return the sign of diff
    return diff ? diff < 0 ? -1 : 1 : 0;
  }

  public lt(frac: Fraction): boolean {
    return this.realValue < frac.realValue;
  }

  public lte(frac: Fraction): boolean {
    return this.realValue <= frac.realValue;
  }

  public gt(frac: Fraction): boolean {
    return !this.lte(frac);
  }

  public gte(frac: Fraction): boolean {
    return !this.lt(frac);
  }

  //public Equals(f: Fraction): boolean {
  //    if (ReferenceEquals(this, f))
  //        return true;
  //    if (ReferenceEquals(f, undefined))
  //        return false;
  //    return this.numerator * f.denominator === f.numerator * this.denominator;
  //}

  private setRealValue(): void {
    this.realValue = this.wholeValue + this.numerator / this.denominator;
  }

  private simplify(): void {
    // don't simplify in case of a GraceNote (need it in order to set the right symbol)
    if (this.numerator === 0) {
      this.denominator = 1;
      return;
    }

    // normally should check if denominator or fraction.denominator is 0 but in our case a zero denominator
    // doesn't make sense. Could probably be optimized
    const i: number = Fraction.greatestCommonDenominator(Math.abs(this.numerator), Math.abs(this.denominator));

    this.numerator /= i;
    this.denominator /= i;

    const whole: number = Math.floor(this.numerator / this.denominator);
    if (whole !== 0) {
      this.wholeValue += whole;
      this.numerator -= whole * this.denominator;
      if (this.numerator === 0) {
        this.denominator = 1;
      }
    }
    if (this.denominator > Fraction.maximumAllowedNumber) {
      const factor: number = this.denominator / Fraction.maximumAllowedNumber;
      this.numerator = Math.round(this.numerator / factor);
      this.denominator = Math.round(this.denominator / factor);
    }
    if (this.numerator > Fraction.maximumAllowedNumber) {
      const factor: number = this.numerator / Fraction.maximumAllowedNumber;
      this.numerator = Math.round(this.numerator / factor);
      this.denominator = Math.round(this.denominator / factor);
    }
  }

  public static FloatInaccuracyTolerance: number = 0.0001; // inaccuracy allowed when comparing Fraction.RealValues, because of floating point inaccuracy

  public isOnBeat(timeSignature: Fraction): boolean { // use sourceMeasure.ActiveTimeSignature as timeSignature
      const beatDistance: number = this.distanceFromBeat(timeSignature);
      return Math.abs(beatDistance) < Fraction.FloatInaccuracyTolerance;
  }

  public distanceFromBeat(timeSignature: Fraction): number {
      const beatStep: Fraction = new Fraction(1, timeSignature.Denominator);
      const distanceFromBeat: number = this.RealValue % beatStep.RealValue; // take modulo the beat value, e.g. 1/8 in a 3/8 time signature
      return distanceFromBeat;
  }


  //private static equals(f1: Fraction, f2: Fraction): boolean {
  //    return f1.numerator * f2.denominator === f2.numerator * f1.denominator;
  //}
  //
  //public static ApproximateFractionFromValue(value: number, epsilonForPrecision: number): Fraction {
  //    let n: number = 1;
  //    let d: number = 1;
  //    let fraction: number = n / d;
  //    while (Math.abs(fraction - value) > epsilonForPrecision) {
  //        if (fraction < value) {
  //            n++;
  //        }
  //        else {
  //            d++;
  //            n = Math.round(value * d);
  //        }
  //        fraction = n / d;
  //    }
  //    return new Fraction(n, d);
  //}
  //public static GetEarlierTimestamp(m1: Fraction, m2: Fraction): Fraction {
  //    if (m1 < m2)
  //        return m1;
  //    else return m2;
  //}

  //public static getFraction(value: number, denominatorPrecision: number): Fraction {
  //    let numerator: number = Math.round(value / (1.0 / denominatorPrecision));
  //    return new Fraction(numerator, denominatorPrecision);
  //}
  //public static fractionMin(f1: Fraction, f2: Fraction): Fraction {
  //    if (f1 < f2)
  //        return f1;
  //    else return f2;
  //}

  //public static GetMaxValue(): Fraction {
  //    return new Fraction(Fraction.maximumAllowedNumber, 1);
  //}
  //public static get MaxAllowedNumerator(): number {
  //    return Fraction.maximumAllowedNumber;
  //}
  //public static get MaxAllowedDenominator(): number {
  //    return Fraction.maximumAllowedNumber;
  //}
  //public ToFloatingString(): string {
  //    return this.RealValue.ToString();
  //}
  //public Compare(x: Fraction, y: Fraction): number {
  //    if (x > y)
  //        return 1;
  //    if (x < y)
  //        return -1;
  //    return 0;
  //}

  //#region operators
  //
  //    // operator overloads must always come in pairs
  //    // operator overload +
  //    public static Fraction operator + (Fraction f1, Fraction f2)
  //{
  //    Fraction sum = new Fraction(f1);
  //    sum.Add(f2);
  //    return sum;
  //}
  //
  //// operator overload -
  //public static Fraction operator - (Fraction f1, Fraction f2)
  //{
  //    Fraction diff = new Fraction(f1);
  //    diff.Sub(f2);
  //    return diff;
  //}
  //
  //// operator overloads must always come in pairs
  //// operator overload >
  //public static bool operator > (Fraction f1, Fraction f2)
  //{
  //    //return (long) f1.Numerator*f2._denominator > (long) f2._numerator*f1._denominator;
  //    return f1.RealValue > f2.RealValue;
  //}
  //
  //// operator overload <
  //public static bool operator < (Fraction f1, Fraction f2)
  //{
  //    //return (long) f1._numerator*f2._denominator < (long) f2._numerator*f1._denominator;
  //    return f1.RealValue < f2.RealValue;
  //}
  //
  //// operator overload ==
  //public static bool operator === (Fraction f1, Fraction f2)
  //{
  //    // code enhanced for performance
  //    // System.Object.ReferenceEquals(f1, undefined) is better than if (f1)
  //    // and comparisons between booleans are quick
  //    bool f1IsNull = System.Object.ReferenceEquals(f1, undefined);
  //    bool f2IsNull = System.Object.ReferenceEquals(f2, undefined);
  //
  //    // method returns true when both are undefined, false when only the first is undefined, otherwise the result of equals
  //    if (f1IsNull !== f2IsNull)
  //        return false;
  //
  //    if (f1IsNull /*&& f2IsNull*/)
  //        return true;
  //
  //    return equals(f1, f2);
  //}
  //
  //// operator overload !=
  //public static bool operator !== (Fraction f1, Fraction f2)
  //{
  //    return (!(f1 === f2));
  //}
  //
  //// operator overload >=
  //public static bool operator >= (Fraction f1, Fraction f2)
  //{
  //    return (!(f1 < f2));
  //}
  //
  //// operator overload <=
  //public static bool operator <= (Fraction f1,Fraction f2)
  //{
  //    return (!(f1 > f2));
  //}
  //
  //public static Fraction operator / (Fraction f, int i)
  //{
  //    return new Fraction(f._numerator, f._denominator *= i);
  //}
  //
  //public static Fraction operator / (Fraction f1, Fraction f2)
  //{
  //    let res = new Fraction(f1.Numerator*f2.Denominator, f1.Denominator*f2.Numerator);
  //    return res.Denominator === 0 ? new Fraction(0, 1) : res;
  //}
  //
  //public static Fraction operator * (Fraction f1, Fraction f2)
  //{
  //    return new Fraction(f1.Numerator*f2.Numerator, f1.Denominator*f2.Denominator);
  //}
  //
  //public static Fraction operator % (Fraction f1, Fraction f2)
  //{
  //    let a = f1/f2;
  //    return new Fraction(a.Numerator%a.Denominator, a.Denominator)*f2;
  //}
  //
  //#endregion operators
}