🎉 Exercism Research is now launched. Help Exercism, help science and have some fun at research.exercism.io 🎉
Avatar of dgeiger

dgeiger's solution

to Rational Numbers in the Delphi Pascal Track

Published at Sep 28 2020 · 0 comments
Instructions
Test suite
Solution

A rational number is defined as the quotient of two integers a and b, called the numerator and denominator, respectively, where b != 0.

The absolute value |r| of the rational number r = a/b is equal to |a|/|b|.

The sum of two rational numbers r1 = a1/b1 and r2 = a2/b2 is r1 + r2 = a1/b1 + a2/b2 = (a1 * b2 + a2 * b1) / (b1 * b2).

The difference of two rational numbers r1 = a1/b1 and r2 = a2/b2 is r1 - r2 = a1/b1 - a2/b2 = (a1 * b2 - a2 * b1) / (b1 * b2).

The product (multiplication) of two rational numbers r1 = a1/b1 and r2 = a2/b2 is r1 * r2 = (a1 * a2) / (b1 * b2).

Dividing a rational number r1 = a1/b1 by another r2 = a2/b2 is r1 / r2 = (a1 * b2) / (a2 * b1) if a2 * b1 is not zero.

Exponentiation of a rational number r = a/b to a non-negative integer power n is r^n = (a^n)/(b^n).

Exponentiation of a rational number r = a/b to a negative integer power n is r^n = (b^m)/(a^m), where m = |n|.

Exponentiation of a rational number r = a/b to a real (floating-point) number x is the quotient (a^x)/(b^x), which is a real number.

Exponentiation of a real number x to a rational number r = a/b is x^(a/b) = root(x^a, b), where root(p, q) is the qth root of p.

Implement the following operations:

  • addition, subtraction, multiplication and division of two rational numbers,
  • absolute value, exponentiation of a given rational number to an integer power, exponentiation of a given rational number to a real (floating-point) power, exponentiation of a real number to a rational number.

Your implementation of rational numbers should always be reduced to lowest terms. For example, 4/4 should reduce to 1/1, 30/60 should reduce to 1/2, 12/8 should reduce to 3/2, etc. To reduce a rational number r = a/b, divide a and b by the greatest common divisor (gcd) of a and b. So, for example, gcd(12, 8) = 4, so r = 12/8 can be reduced to (12/4)/(8/4) = 3/2.

Assume that the programming language you are using does not have an implementation of rational numbers.

Hints

  • Operator overloading is being introduced in this exercise. The Embarcadero docwiki on the subject will be very helpful to you in understanding how overriding class operators is possible along with Implicit and Explicit casting.

Testing

In order to run the tests for this track, you will need to install DUnitX. Please see the installation instructions for more information.

Loading Exercises into Delphi

If Delphi is properly installed, and *.dpr file types have been associated with Delphi, then double clicking the supplied *.dpr file will start Delphi and load the exercise/project. control + F9 is the keyboard shortcut to compile the project or pressing F9 will compile and run the project.

Alternatively you may opt to start Delphi and load your project via. the File drop down menu.

When Questions Come Up

We monitor the Pascal-Delphi support room on gitter.im to help you with any questions that might arise.

Submitting Exercises

Note that, when trying to submit an exercise, make sure the exercise file you're submitting is in the exercism/delphi/<exerciseName> directory.

For example, if you're submitting ubob.pas for the Bob exercise, the submit command would be something like exercism submit <path_to_exercism_dir>/delphi/bob/ubob.pas.

Source

Wikipedia https://en.wikipedia.org/wiki/Rational_number

Submitting Incomplete Solutions

It's possible to submit an incomplete solution so you may request help from a mentor.

RationalNumbersTest.dpr

program RationalNumbersTest;

{$IFNDEF TESTINSIGHT}
{$APPTYPE CONSOLE}
{$ENDIF}{$STRONGLINKTYPES ON}
uses
  System.SysUtils,
  {$IFDEF TESTINSIGHT}
  TestInsight.DUnitX,
  {$ENDIF }
  DUnitX.Loggers.Console,
  DUnitX.Loggers.Xml.NUnit,
  DUnitX.TestFramework,
  uRationalNumbersTests in 'uRationalNumbersTests.pas',
  uRationalNumbers in 'uRationalNumbers.pas';

var
  runner : ITestRunner;
  results : IRunResults;
  logger : ITestLogger;
  nunitLogger : ITestLogger;
begin
{$IFDEF TESTINSIGHT}
  TestInsight.DUnitX.RunRegisteredTests;
  exit;
{$ENDIF}
  try
    //Check command line options, will exit if invalid
    TDUnitX.CheckCommandLine;
    //Create the test runner
    runner := TDUnitX.CreateRunner;
    //Tell the runner to use RTTI to find Fixtures
    runner.UseRTTI := True;
    //tell the runner how we will log things
    //Log to the console window
    logger := TDUnitXConsoleLogger.Create(true);
    runner.AddLogger(logger);
    //Generate an NUnit compatible XML File
    nunitLogger := TDUnitXXMLNUnitFileLogger.Create(TDUnitX.Options.XMLOutputFile);
    runner.AddLogger(nunitLogger);
    runner.FailsOnNoAsserts := True; //When true, Assertions must be made during tests;

    //Run tests
    results := runner.Execute;
    if not results.AllPassed then
      System.ExitCode := EXIT_ERRORS;

    {$IFNDEF CI}
    //We don't want this happening when running under CI.
    if TDUnitX.Options.ExitBehavior = TDUnitXExitBehavior.Pause then
    begin
      System.Write('Done.. press <Enter> key to quit.');
      System.Readln;
    end;
    {$ENDIF}
  except
    on E: Exception do
      System.Writeln(E.ClassName, ': ', E.Message);
  end;
end.

uRationalNumbersTests.pas

unit uRationalNumbersTests;

interface
uses
  DUnitX.TestFramework;

const
  CanonicalVersion = '1.1.0.2';

type

  [TestFixture('Addition')]
  TAdditionTests = class(TObject)
  public
    [Test]
//    [Ignore('Comment the "[Ignore]" statement to run the test')]
    procedure AddTwoPositiveRationalNumbers;

    [Test]
    [Ignore]
    procedure AddAPositiveRationalNumberAndANegativeRationalNumber;

    [Test]
    [Ignore]
    procedure AddTwoNegativeRationalNumbers;

    [Test]
    [Ignore]
    procedure AddARationalNumberToItsAdditiveInverse;
  end;

  [TestFixture('Subtraction')]
  TSubtractionTests = class(TObject)
  public
    [Test]
    [Ignore]
    procedure SubtractTwoPositiveRationalNumbers;

    [Test]
    [Ignore]
    procedure SubtractAPositiveRationalNumberAndANegativeRationalNumber;

    [Test]
    [Ignore]
    procedure SubtractTwoNegativeRationalNumbers;

    [Test]
    [Ignore]
    procedure SubtractARationalNumberFromItself;
  end;

  [TestFixture('Multiplication')]
  TMultiplicationTests = class(TObject)
  public
    [Test]
    [Ignore]
    procedure MultiplyTwoPositiveRationalNumbers;

    [Test]
    [Ignore]
    procedure MultiplyANegativeRationalNumberByAPositiveRationalNumber;

    [Test]
    [Ignore]
    procedure MultiplyTwoNegativeRationalNumbers;

    [Test]
    [Ignore]
    procedure MultiplyARationalNumberByItsReciprocal;

    [Test]
    [Ignore]
    procedure MultiplyARationalNumberByOne;

    [Test]
    [Ignore]
    procedure MultiplyARationalNumberByZero;
  end;

  [TestFixture('Division')]
  TDivisionTests = class(TObject)
  public
    [Test]
    [Ignore]
    procedure DivideTwoPositiveRationalNumbers;

    [Test]
    [Ignore]
    procedure DivideAPositiveRationalNumberByANegativeRationalNumber;

    [Test]
    [Ignore]
    procedure DivideTwoNegativeRationalNumbers;

    [Test]
    [Ignore]
    procedure DivideARationalNumberByOne;

    [Test]
    [Ignore]
    procedure DivideAWholeNumberByARationalNumber;
  end;

  [TestFixture('Absolute value')]
  TAbsoluteValueTests = class(TObject)
  public
    [Test]
    [Ignore]
    procedure AbsoluteValueOfAPositiveRationalNumber;

    [Test]
    [Ignore]
    procedure AbsoluteValueOfAPositiveRationalNumberWithNegativeNumeratorAndDenominator;

    [Test]
    [Ignore]
    procedure AbsoluteValueOfANegativeRationalNumber;

    [Test]
    [Ignore]
    procedure AbsoluteValueOfANegativeRationalNumberWithNegativeDenominator;

    [Test]
    [Ignore]
    procedure AbsoluteValueOfZero;
  end;

  [TestFixture('Exponentiation of a rational number')]
  TExpoRationalNumberTests = class(TObject)
  public
    [Test]
    [Ignore]
    procedure RaiseAPositiveRationalNumberToAPositiveIntegerPower;

    [Test]
    [Ignore]
    procedure RaiseANegativeRationalNumberToAPositiveIntegerPower;

    [Test]
    [Ignore]
    procedure RaiseZeroToAnIntegerPower;

    [Test]
    [Ignore]
    procedure RaiseOneToAnIntegerPower;

    [Test]
    [Ignore]
    procedure RaiseAPositiveRationalNumberToThePowerOfZero;

    [Test]
    [Ignore]
    procedure RaiseANegativeRationalNumberToThePowerOfZero;
  end;

  [TestFixture('Exponentiation of a real number to a rational number')]
  TExpoRealToRatNumber = class(TObject)
  public
    [Test]
    [Ignore]
    procedure RaiseARealNumberToAPositiveRationalNumber;

    [Test]
    [Ignore]
    procedure RaiseARealNumberToANegativeRationalNumber;

    [Test]
    [Ignore]
    procedure RaiseARealNumberToAZeroRationalNumber;
  end;

  [TestFixture('Reduction to lowest terms')]
  TReduceTests = class(TObject)
  public
    [Test]
    [Ignore]
    procedure ReduceAPositiveRationalNumberToLowestTerms;

    [Test]
    [Ignore]
    procedure ReduceANegativeRationalNumberToLowestTerms;

    [Test]
    [Ignore]
    procedure ReduceARationalNumberWithANegativeDenominatorToLowestTerms;

    [Test]
    [Ignore]
    procedure ReduceZeroToLowestTerms;

    [Test]
    [Ignore]
    procedure ReduceAnIntegerToLowestTerms;

    [Test]
    [Ignore]
    procedure ReduceOneToLowestTerms;
  end;

implementation
uses
  System.Math, uRationalNumbers;


{$region 'TAdditionTests'}

procedure TAdditionTests.AddAPositiveRationalNumberAndANegativeRationalNumber;
var
  LPositiveRationalValue: TFraction;
  LNegativeRationalValue: TFraction;
  Expected: string;
  Actual: TFraction;
begin
  Expected := '-1/6';
  LPositiveRationalValue := TFraction.CreateFrom(1, 2);
  LNegativeRationalValue := TFraction.CreateFrom(2, 3);
  Actual := LPositiveRationalValue + -LNegativeRationalValue;
  Assert.AreEqual(Expected, string(Actual));
end;

procedure TAdditionTests.AddARationalNumberToItsAdditiveInverse;
var
  LRationalNumber: TFraction;
  Actual: TFraction;
  Expected: string;
begin
  Expected := '0/1';
  LRationalNumber := TFraction.CreateFrom(1, 2);
  Actual := LRationalNumber + -LRationalNumber;
  Assert.AreEqual(Expected, string(actual));
end;

procedure TAdditionTests.AddTwoNegativeRationalNumbers;
var
  lNegFracA: TFraction;
  lNegFracB: TFraction;
  Actual: TFraction;
  Expected: string;
begin
  Expected := '-7/6';
  LNegFracA := TFraction.CreateFrom(-1, 2);
  LNegFracB := TFraction.CreateFrom(-2 ,3);
  Actual := LNegFracA + LNegFracB;
  Assert.AreEqual(Expected, string(actual));
end;

procedure TAdditionTests.AddTwoPositiveRationalNumbers;
var
  lPosFracA: TFraction;
  lPosFracB: TFraction;
  Actual: TFraction;
  Expected: string;
begin
  Expected := '7/6';
  LPosFracA := TFraction.CreateFrom(1, 2);
  LPosFracB := TFraction.CreateFrom(2 ,3);
  Actual := LPosFracA + LPosFracB;
  Assert.AreEqual(Expected, string(actual));
end;
{$endregion}

{$region 'TSubtractionTests'}

procedure TSubtractionTests.SubtractAPositiveRationalNumberAndANegativeRationalNumber;
var
  LPosRatNum: TFraction;
  LNegRatNum: TFraction;
  Actual: TFraction;
  Expected: string;
begin
  Expected := '7/6';
  LPosRatNum := TFraction.CreateFrom(1, 2);
  LNegRatNum := TFraction.CreateFrom(-2, 3);
  Actual := LPosRatNum - LNegRatNum;
  Assert.AreEqual(Expected, string(Actual));
end;

procedure TSubtractionTests.SubtractARationalNumberFromItself;
var
  LRatNum: TFraction;
  Actual: TFraction;
  Expected: string;
begin
  Expected := '0/1';
  LRatNum := TFraction.CreateFrom(1, 2);
  Actual := LRatNum - LRatNum;
  Assert.AreEqual(Expected, string(Actual));
end;

procedure TSubtractionTests.SubtractTwoNegativeRationalNumbers;
var
  LNegRatNumA: TFraction;
  LNegRatNumB: TFraction;
  Actual: TFraction;
  Expected: string;
begin
  Expected := '1/6';
  LNegRatNumA := TFraction.CreateFrom(-1, 2);
  LNegRatNumB := TFraction.CreateFrom(-2, 3);
  Actual := LNegRatNumA - LNegRatNumB;
  Assert.AreEqual(Expected, string(Actual));
end;

procedure TSubtractionTests.SubtractTwoPositiveRationalNumbers;
var
  LPosRatNumA: TFraction;
  LPosRatNumB: TFraction;
  Actual: TFraction;
  Expected: string;
begin
  Expected := '-1/6';
  LPosRatNumA := TFraction.CreateFrom(1, 2);
  LPosRatNumB := TFraction.CreateFrom(2, 3);
  Actual := LPosRatNumA - LPosRatNumB;
  Assert.AreEqual(Expected, string(Actual));
end;
{$endregion}

{$region 'TMultiplicationTests'}

procedure TMultiplicationTests.MultiplyANegativeRationalNumberByAPositiveRationalNumber;
var
  LNegRatA: TFraction;
  LPosRatB: TFraction;
  Actual: TFraction;
  Expected: string;
begin
  Expected := '-1/3';
  LNegRatA := TFraction.CreateFrom(-1, 2);
  LPosRatB := TFraction.CreateFrom(2, 3);
  Actual := LNegRatA * LPosRatB;
  Assert.AreEqual(Expected, string(Actual));
end;

procedure TMultiplicationTests.MultiplyARationalNumberByItsReciprocal;
var
  LRatNum: TFraction;
  Actual: TFraction;
  Expected: string;
begin
  Expected := '1/1';
  LRatNum := TFraction.CreateFrom(1, 2);
  Actual := LRatNum * (1 / LRatNum);
  Assert.AreEqual(Expected, string(Actual));
end;

procedure TMultiplicationTests.MultiplyARationalNumberByOne;
var
  LRatNum: TFraction;
  Actual: TFraction;
  Expected: string;
begin
  Expected := '1/2';
  LRatNum := TFraction.CreateFrom(1, 2);
  Actual := LRatNum * 1;
  Assert.AreEqual(Expected, string(Actual));
end;

procedure TMultiplicationTests.MultiplyARationalNumberByZero;
var
  LRatNum: TFraction;
  Actual: TFraction;
  Expected: string;
begin
  Expected := '0/1';
  LRatNum := TFraction.CreateFrom(1, 2);
  Actual := LRatNum * 0;
  Assert.AreEqual(Expected, string(Actual));
end;

procedure TMultiplicationTests.MultiplyTwoNegativeRationalNumbers;
var
  LNegRatA: TFraction;
  LNegRatB: TFraction;
  Actual: TFraction;
  Expected: string;
begin
  Expected := '1/3';
  LNegRatA := TFraction.CreateFrom(-1, 2);
  LNegRatB := TFraction.CreateFrom(-2, 3);
  Actual := LNegRatA * LNegRatB;
  Assert.AreEqual(Expected, string(Actual));
end;

procedure TMultiplicationTests.MultiplyTwoPositiveRationalNumbers;
var
  LPosRatA: TFraction;
  LPosRatB: TFraction;
  Actual: TFraction;
  Expected: string;
begin
  Expected := '1/3';
  LPosRatA := TFraction.CreateFrom(1, 2);
  LPosRatB := TFraction.CreateFrom(2, 3);
  Actual := LPosRatA * LPosRatB;
  Assert.AreEqual(Expected, string(Actual));
end;
{$endregion}

{$region 'TDivisionTests'}

procedure TDivisionTests.DivideAPositiveRationalNumberByANegativeRationalNumber;
var
  LPosRatA: TFraction;
  LNegRatB: TFraction;
  Actual: TFraction;
  Expected: string;
begin
  Expected := '-3/4';
  LPosRatA := TFraction.CreateFrom(1, 2);
  LNegRatB := TFraction.CreateFrom(-2, 3);
  Actual := LPosRatA / LNegRatB;
  Assert.AreEqual(Expected, string(Actual));
end;

procedure TDivisionTests.DivideARationalNumberByOne;
var
  LRatNum: TFraction;
  Actual: TFraction;
  Expected: string;
begin
  Expected := '1/2';
  LRatNum := TFraction.CreateFrom(1, 2);
  Actual := LRatNum / 1;
  Assert.AreEqual(Expected, string(Actual));
end;

procedure TDivisionTests.DivideAWholeNumberByARationalNumber;
var
  LRatNum: TFraction;
  Actual: TFraction;
  Expected: string;
begin
  Expected := '14/1';
  LRatNum := TFraction.CreateFrom(2, 7);
  Actual := 4 / LRatNum;
  Assert.AreEqual(Expected, string(Actual));
end;

procedure TDivisionTests.DivideTwoNegativeRationalNumbers;
var
  LNegRatA: TFraction;
  LNegRatB: TFraction;
  Actual: TFraction;
  Expected: string;
begin
  Expected := '3/4';
  LNegRatA := TFraction.CreateFrom(-1, 2);
  LNegRatB := TFraction.CreateFrom(-2, 3);
  Actual := LNegRatA / LNegRatB;
  Assert.AreEqual(Expected, string(Actual));
end;

procedure TDivisionTests.DivideTwoPositiveRationalNumbers;
var
  LPosRatA: TFraction;
  LPosRatB: TFraction;
  Actual: TFraction;
  Expected: string;
begin
  Expected := '3/4';
  LPosRatA := TFraction.CreateFrom(1, 2);
  LPosRatB := TFraction.CreateFrom(2, 3);
  Actual := LPosRatA / LPosRatB;
  Assert.AreEqual(Expected, string(Actual));
end;
{$endregion}

{$region 'TAbsoluteValueTests'}

procedure TAbsoluteValueTests.AbsoluteValueOfANegativeRationalNumber;
var
  LNegRatNum: TFraction;
  Actual: TFraction;
  Expected: string;
begin
  Expected := '1/2';
  LNegRatNum := TFraction.CreateFrom(-1, 2);
  Actual := TFraction(Abs(LNegRatNum));
  Assert.AreEqual(Expected, string(Actual));
end;

procedure TAbsoluteValueTests.AbsoluteValueOfANegativeRationalNumberWithNegativeDenominator;
var
  LNegRatNum: TFraction;
  Actual: TFraction;
  Expected: string;
begin
  Expected := '1/2';
  LNegRatNum := TFraction.CreateFrom(1, -2);
  Actual := TFraction(Abs(LNegRatNum));
  Assert.AreEqual(Expected, string(Actual));
end;

procedure TAbsoluteValueTests.AbsoluteValueOfAPositiveRationalNumber;
var
  LPosRatNum: TFraction;
  Actual: TFraction;
  Expected: string;
begin
  Expected := '1/2';
  LPosRatNum := TFraction.CreateFrom(1, 2);
  Actual := TFraction(Abs(LPosRatNum));
  Assert.AreEqual(Expected, string(Actual));
end;

procedure TAbsoluteValueTests.AbsoluteValueOfAPositiveRationalNumberWithNegativeNumeratorAndDenominator;
var
  LNegRatNum: TFraction;
  Actual: TFraction;
  Expected: string;
begin
  Expected := '1/2';
  LNegRatNum := TFraction.CreateFrom(-1, -2);
  Actual := TFraction(Abs(LNegRatNum));
  Assert.AreEqual(Expected, string(Actual));
end;

procedure TAbsoluteValueTests.AbsoluteValueOfZero;
var
  LZero: TFraction;
  Actual: TFraction;
  Expected: string;
begin
  Expected := '0/1';
  LZero := TFraction.CreateFrom(0, 1);
  Actual := TFraction(Abs(LZero));
  Assert.AreEqual(Expected, string(Actual));
end;
{$endregion}

{$region 'TExpoRationalNumberTests'}

procedure TExpoRationalNumberTests.RaiseANegativeRationalNumberToAPositiveIntegerPower;
var
  NegRatNum: TFraction;
  Actual: TFraction;
  Expected: string;
begin
  Expected := '-1/8';
  NegRatNum := TFraction.CreateFrom(-1, 2);
  Actual := TFraction(System.Math.Power(NegRatNum,3));
  Assert.AreEqual(Expected, string(Actual));
end;

procedure TExpoRationalNumberTests.RaiseANegativeRationalNumberToThePowerOfZero;
var
  NegRatNum: TFraction;
  Actual: TFraction;
  Expected: string;
begin
  Expected := '1/1';
  NegRatNum := TFraction.CreateFrom(-1, 2);
  Actual := TFraction(System.Math.Power(NegRatNum,0));
  Assert.AreEqual(Expected, string(Actual));
end;

procedure TExpoRationalNumberTests.RaiseAPositiveRationalNumberToAPositiveIntegerPower;
var
  PosRatNum: TFraction;
  Actual: TFraction;
  Expected: string;
begin
  Expected := '1/8';
  PosRatNum := TFraction.CreateFrom(1, 2);
  Actual := TFraction(System.Math.Power(PosRatNum, 3));
  Assert.AreEqual(Expected, string(Actual));
end;

procedure TExpoRationalNumberTests.RaiseAPositiveRationalNumberToThePowerOfZero;
var
  PosRatNum: TFraction;
  Actual: TFraction;
  Expected: string;
begin
  Expected := '1/1';
  PosRatNum := TFraction.CreateFrom(1, 2);
  Actual := TFraction(System.Math.Power(PosRatNum, 0));
  Assert.AreEqual(Expected, string(Actual));
end;

procedure TExpoRationalNumberTests.RaiseOneToAnIntegerPower;
var
  Actual: TFraction;
  Expected: string;
begin
  Expected := '1/1';
  Actual := TFraction(System.Math.Power(1, 4));
  Assert.AreEqual(Expected, string(Actual));
end;

procedure TExpoRationalNumberTests.RaiseZeroToAnIntegerPower;
var
  Actual: TFraction;
  Expected: string;
begin
  Expected := '0/1';
  Actual := TFraction(System.Math.Power(0, 5));
  Assert.AreEqual(Expected, string(Actual));
end;
{$endregion}

{$region 'TExpoRealToRatNumber'}

procedure TExpoRealToRatNumber.RaiseARealNumberToANegativeRationalNumber;
var
  NegRatNum: TFraction;
  Actual: Double;
  Expected: Double;
begin
  Expected := 1 / 3;
  NegRatNum := TFraction.CreateFrom(-1, 2);
  Actual := System.Math.Power(9, NegRatNum);
  Assert.AreEqual(Expected, Actual);
end;

procedure TExpoRealToRatNumber.RaiseARealNumberToAPositiveRationalNumber;
var
  PosRatNum: TFraction;
  Actual: Double;
  Expected: Double;
begin
  Expected := 16.0;
  PosRatNum := TFraction.CreateFrom(4, 3);
  Actual := System.Math.Power(8, PosRatNum);
  Assert.AreEqual(Expected, Actual);
end;

procedure TExpoRealToRatNumber.RaiseARealNumberToAZeroRationalNumber;
var
  ZeroRatNum: TFraction;
  Actual: Double;
  Expected: Double;
begin
  Expected := 1.0;
  ZeroRatNum := TFraction.CreateFrom(0, 1);
  Actual := System.Math.Power(2, ZeroRatNum);
  Assert.AreEqual(Expected, Actual);
end;
{$endregion}

{$region 'TReduceTests'}

procedure TReduceTests.ReduceANegativeRationalNumberToLowestTerms;
var
  Actual: TFraction;
  Expected: string;
begin
  Expected := '-2/3';
  Actual := TFraction.CreateFrom(-4, 6).Reduced;
  Assert.AreEqual(Expected, string(Actual));
end;

procedure TReduceTests.ReduceAnIntegerToLowestTerms;
var
  Actual: TFraction;
  Expected: string;
begin
  Expected := '-2/1';
  Actual := TFraction.CreateFrom(-14, 7).Reduced;
  Assert.AreEqual(Expected, string(Actual));
end;

procedure TReduceTests.ReduceAPositiveRationalNumberToLowestTerms;
var
  Actual: TFraction;
  Expected: string;
begin
  Expected := '1/2';
  Actual := TFraction.CreateFrom(2, 4).Reduced;
  Assert.AreEqual(Expected, string(Actual));
end;

procedure TReduceTests.ReduceARationalNumberWithANegativeDenominatorToLowestTerms;
var
  Actual: TFraction;
  Expected: string;
begin
  Expected := '-1/3';
  Actual := TFraction.CreateFrom(3, -9).Reduced;
  Assert.AreEqual(Expected, string(Actual));
end;

procedure TReduceTests.ReduceOneToLowestTerms;
var
  Actual: TFraction;
  Expected: string;
begin
  Expected := '1/1';
  Actual := TFraction.CreateFrom(13, 13).Reduced;
  Assert.AreEqual(Expected, string(Actual));
end;

procedure TReduceTests.ReduceZeroToLowestTerms;
var
  Actual: TFraction;
  Expected: string;
begin
  Expected := '0/1';
  Actual := TFraction.CreateFrom(0, 6).Reduced;
  Assert.AreEqual(Expected, string(Actual));
end;
{$endregion}

initialization
  TDUnitX.RegisterTestFixture(TAdditionTests);
  TDUnitX.RegisterTestFixture(TSubtractionTests);
  TDUnitX.RegisterTestFixture(TMultiplicationTests);
  TDUnitX.RegisterTestFixture(TDivisionTests);
  TDUnitX.RegisterTestFixture(TAbsoluteValueTests);
  TDUnitX.RegisterTestFixture(TExpoRationalNumberTests);
  TDUnitX.RegisterTestFixture(TExpoRealToRatNumber);
  TDUnitX.RegisterTestFixture(TReduceTests);
end.
unit uRationalNumbers;

interface

uses
  System.SysUtils;

type
  TFraction = record
    private
      FNumerator: Integer;
      FDenominator: Integer;

      function GCD(A, B: Integer): Integer;

    public
      class operator Add(A: TFraction; B: TFraction): TFraction;
      class operator Divide(A: Integer; B: TFraction): TFraction;
      class operator Divide(A: TFraction; B: Integer): TFraction;
      class operator Divide(A: TFraction; B: TFraction): TFraction;
      class operator Implicit(A: Double): TFraction;
      class operator Implicit(A: TFraction): Double;
      class operator Implicit(A: TFraction): String;
      class operator Multiply(A: TFraction; B: Integer): TFraction;
      class operator Multiply(A: TFraction; B: TFraction): TFraction;
      class operator Negative(Fraction: TFraction): TFraction;
      class operator Subtract(A: TFraction; B: TFraction): TFraction;

      constructor CreateFrom(Numerator: Integer; Denominator: Integer);

      function Reduced: TFraction;

  end;

  function Abs(F: TFraction): Double;

implementation

function Abs(F: TFraction): Double;
begin
  // Is the numerator negative?
  if F.FNumerator < 0 then
    // Yes, force it positive
    F.FNumerator := -F.FNumerator;

  // Is the denominator negative?
  if F.FDenominator < 0 then
    // Yes, force it positive
    F.FDenominator := -F.FDenominator;

  // Do the division to get the double value
  Result := F.FNumerator / F.FDenominator;
end;

{ TFraction }

class operator TFraction.Add(A, B: TFraction): TFraction;
var
  Denominator: Integer;
  Numerator: Integer;
begin
  // Calculate the new numerator for the sum
  Numerator := (A.FNumerator * B.FDenominator) + (A.FDenominator * B.FNumerator);

  // Calculate the new denominator for the sum
  Denominator := A.FDenominator * B.FDenominator;

  // If the new denominator is negative,
  if Denominator < 0 then
    begin
      // flip the sign on the new numerator,
      Numerator := -Numerator;

      // and force the new denominator positive
      Denominator := -Denominator;
    end;

  // Create a new fraction using the new numerator and new denominator
  Result := TFraction.CreateFrom(Numerator, Denominator);
end;

constructor TFraction.CreateFrom(Numerator, Denominator: Integer);
var
  NewDenominator: Integer;
begin
  // Calculate the new denominator by calculating the GCD and using that as
  // the new denominator
  NewDenominator := GCD(Numerator, Denominator);

  // Divide the numerator by the GCD
  FNumerator := Numerator div NewDenominator;

  // Divide the denominator by the GCD
  FDenominator := Denominator div NewDenominator;

  // Safety check - is the denominator negative?
  if FDenominator < 0 then
    begin
      // Yes, flip the sign of the numerator,
      FNumerator := -FNumerator;

      // and force the denominator negative.
      FDenominator := -FDenominator;
    end;
end;

class operator TFraction.Divide(A: Integer; B: TFraction): TFraction;
begin
  // Divide integer by the fraction by creating a new fraction by
  // multiplying the by the inverse of the fraction
  Result := TFraction.CreateFrom(A * B.FDenominator, B.FNumerator);
end;

class operator TFraction.Divide(A: TFraction; B: Integer): TFraction;
begin
  // Divide the fraction by the integer by creating a new fraction by
  // multiplying the denominator by the integer
  Result := TFraction.CreateFrom(A.FNumerator, A.FDenominator * B);
end;

class operator TFraction.Divide(A, B: TFraction): TFraction;
begin
  // Divide one fraction by another, creating a new fraction that
  // multiplies the first fraction by the inverse of the second
  Result := TFraction.CreateFrom(A.FNumerator * B.FDenominator, A.FDenominator * B.FNumerator);
end;

function TFraction.GCD(A, B: Integer): Integer;
var
  Remainder: Integer;
begin
  // Use Euclid's method to calculate the GCD

  // If either number is zero, we need to use the other number as the GCD,
  // since the attempt to calculate will cause a division by zero
  if A = 0 then
    begin
      Result := B;

      Exit;
    end;

  // If either number is zero, we need to use the other number as the GCD,
  // since the attempt to calculate will cause a division by zero
  if B = 0 then
    begin
      Result := A;

      exit;
    end;

  // Make sure that both numbers are positive
  A := System.Abs(A);
  B := System.Abs(B);

  // While the second number is positive
  while B <> 0 do
    begin
      // Get the remainder of integer division
      Remainder := A mod B;

      // Set the first number to the value of the second
      A := B;

      // and then set the second number to the remainder
      B := Remainder;
    end;

  // The first number will be the GCD, and the second wioll be zero
  Result := A;
end;

class operator TFraction.Implicit(A: TFraction): Double;
begin
  // Calculate the double value of the fraction
  Result := A.FNumerator / A.FDenominator;
end;

class operator TFraction.Implicit(A: Double): TFraction;
var
  Numerator: Double;
  Denominator: Integer;
  Int: Integer;
  Delta: Double;
begin
  // The current value of the numerator is the decimal portion of the double
  Numerator := Frac(A);

  // and the integer portion is saved for later addition
  Int := Trunc(A);

  // Start by multiplying by ten
  Denominator := 10;

  // Calculate the difference between the decimal multiplied by the denominator
  // and the now-integer portion of the decimal multiplied by the denominator
  Delta := (Frac(A) * Denominator) - (Trunc(Numerator) * Denominator);

  // While there is a non-zero difference,
  while System.Abs(Delta) > 0 do
    begin
      // calculate the new delta
      Delta := (Frac(A) * Denominator) - Trunc(Numerator * Denominator);

      // and prepare to use a new denominator
      Denominator := Denominator * 10;
  end;

  // Calculate the actual denominator to use for the fraction
  Numerator := Trunc(Numerator * Denominator);

  // Create a new fraction by using the new numerator and denominator, and then
  // adding the original integer portion
  Result := TFraction.CreateFrom(Trunc(Numerator), Denominator) +
            TFraction.CreateFrom(Trunc(A), 1);
end;

class operator TFraction.Implicit(A: TFraction): String;
begin
  // Convert the numbers to a string and concatenate them
  Result := IntToStr(A.FNumerator) + '/' + IntToStr(A.FDenominator);
end;

class operator TFraction.Multiply(A, B: TFraction): TFraction;
var
  Numerator, Denominator: Integer;
begin
  // To multiply two fractions, first multiple the numerators
  Numerator   := A.FNumerator * B.FNumerator;

  // and the multiply the denominators
  Denominator := A.FDenominator * B.FDenominator;

  // Create a new fraction using the new numerator and denominator
  Result := TFraction.CreateFrom(Numerator, Denominator);
end;

class operator TFraction.Multiply(A: TFraction; B: Integer): TFraction;
begin
  // Multiple the fraction by the integer by creating a new fraction where
  // the new numerator is the old numerator multiplied by the integer, and
  // keeping the same denominator
  Result := TFraction.CreateFrom(A.FNumerator * B, A.FDenominator);
end;

class operator TFraction.Negative(Fraction: TFraction): TFraction;
begin
  // Flip the sign of the fraction by creating a new fraction with the sign
  // of the numerator flipped
  Result := TFraction.CreateFrom(-Fraction.FNumerator, Fraction.FDenominator);
end;

function TFraction.Reduced: TFraction;
begin
  // As part of the fraction's creation, I already maintain the fraction in
  // it's reduced form
  Result := Self;
end;

class operator TFraction.Subtract(A, B: TFraction): TFraction;
var
  Denominator: Integer;
  Numerator: Integer;
begin
  // Make sure the two fractions are using the same denominator by multiplying
  // the first numerator by the second denominator, then subtracting the
  // second numerator multiplied by the first denominator,
  Numerator := (A.FNumerator * B.FDenominator) - (A.FDenominator * B.FNumerator);

  // and multiplying the two denominators to get the new denominator.
  Denominator := A.FDenominator * B.FDenominator;

  // If the denominator is negative,
  if Denominator < 0 then
    begin
      // Flip the sign of the numerator
      Numerator := -Numerator;

      // and the denominator
      Denominator := -Denominator;
    end;

  // Return the new fraction created from the new numerator and the new denominator
  Result := TFraction.CreateFrom(Numerator, Denominator);
end;

end.

Community comments

Find this solution interesting? Ask the author a question to learn more.

What can you learn from this solution?

A huge amount can be learned from reading other people’s code. This is why we wanted to give exercism users the option of making their solutions public.

Here are some questions to help you reflect on this solution and learn the most from it.

  • What compromises have been made?
  • Are there new concepts here that you could read more about to improve your understanding?