4
\$\begingroup\$

Background:

Equation 1: 2x-y=6

Equation 2: 4x+3y=22

Because 1 is ax-by=c and 2 is dx-ey=f

y=(a * f - c * d) / (a * e - b * d) and x is simply (c - (b * y)) / a)

What I am trying to do:

Ultimately, generate linear equations that can be solved simultaneously for practice (which the code I wrote below can do). So I am creating variations of Equations 1 and 2 above that have whole number solutions for x and y. I do this by:

1.Creating random numbers.

2.Use the above calculation to find x and y from these numbers.

3.Ensure that x and y are whole numbers and if not, create new numbers for a-f until they are.

Working code:

The function NumberGeneration() is called with a button click.

double a;
double b;
double c;
double d;
double e;
double f;
double x;
double y;
System.Random rnd = new System.Random();
string question;
string answer;

public void NumberGeneration()//1.Generates the variables, calculates x and y.
{
    a = rnd.Next(1, 10);
    b = rnd.Next(1, 10);
    c = rnd.Next(1, 10);
    d = rnd.Next(1, 10);
    e = rnd.Next(1, 10);
    f = rnd.Next(1, 10);
    y = ((a * f - c * d) / (a * e - b * d));
    x = ((c - (b * y)) / a);
    CheckForZeros();
}

void CheckForZeros() //2.Checking for zeros
{
    if ((a * e - b * d) == 0) //To avoid dividing by 0, if this is 0 generate new numbers.
    {
        NumberGeneration();
    }
    else if((a * e - b * d) != 0) // otherwise proceed to make sure x and y are whole numbers
    {
        EnsureWhole();
    }

}

void EnsureWhole()
{
    decimal dy = Convert.ToDecimal(y);
    decimal dx = Convert.ToDecimal(x);
    if ((dy % 1) == 0 && (dx % 1) == 0) //3.If both x and y are whole numbers, setup the question with these numbers
    {
        question = string.Format($"{a}x+{b}y={c}, \n {d}x+{e}y={f}", a, b, c, d, e, f);
        answer = string.Format($"x={x} and y={y}");
    }
    else if((dy % 1) != 0 || (dx % 1) != 0)//Otherwise start again, generate a new set of numbers and attempt for a new answer where x and y are ints
    {
        NumberGeneration();
    };
}

Questions:

  • What is the proper way to do this?
  • How to optimize the code?
  • Is this the optimal way to generate random numbers?

Bonus follow-up: Do you have any personal resource suggestions for learning C#? I get that instinctive feeling that my methods have holes in them, I know I am not good at coding, I just don't have the education to know better and I want to educate myself. Even better within the context of maths.

\$\endgroup\$
1
  • \$\begingroup\$ It will help reviewers if you show the full class and how you intent to use it through af simple use case. \$\endgroup\$
    – user73941
    Commented Mar 12, 2020 at 10:30

1 Answer 1

2
\$\begingroup\$

The good thing is, that you have made some meaningful methods with descriptive names.

But you have a quite peculiar workflow:

public void NumberGeneration()//1.Generates the variables, calculates x and y.
{
    a = rnd.Next(1, 10);
    b = rnd.Next(1, 10);
    c = rnd.Next(1, 10);
    d = rnd.Next(1, 10);
    e = rnd.Next(1, 10);
    f = rnd.Next(1, 10);
    y = ((a * f - c * d) / (a * e - b * d));
    x = ((c - (b * y)) / a);
    CheckForZeros();
}

Here you check for dividing by zero after you do the calculation that is supposed to be guarded by the check.

And calling NumberGeneration() recursively from CheckForZeros() and EnsureWhole() if something goes wrong has the potential to end in a stack overflow, if all the generated sets of values fail in the two tests. This is not a good use of recursion, instead you should use an iterative approach and provide a value for max retries before the generator returns unsuccessfully.


Have you considered this condition properly:

  else if ((dy % 1) != 0 || (dx % 1) != 0)//Otherwise start again, generate a new set of numbers and attempt for a new answer where x and y are ints
  {
    NumberGeneration();
  };

Why must at least one of the result values in the current failed calculation be a decimal number in order to allow a new calculation with an entire new set of data? It makes no sense to me.


If everything go well you end the calculations by formatting a pair of private strings and then nothing happens:

    question = string.Format($"{a}x+{b}y={c}, \n {d}x+{e}y={f}", a, b, c, d, e, f);
    answer = string.Format($"x={x} and y={y}");

I imagine that you provide these results to the client in some way or else all the efforts seem useless :-)


In

(dy % 1) != 0

(a * e - b * d) == 0

The parenteses are unnecessary.

And so are the outmost parenteses in this expression:

y = ((a * f - c * d) / (a * e - b * d))


I think you normally will solve the system with + between the parts:

  ax + by = c
  dx + ey = f

  <=>

  y = (cd - af) / (bd - ae)
  x = (c - by) / a

The randomly generated input values (a..f) are all integers, so you could do all calculations as integer calculations, because you only want solutions with integral x and y. That will make the calculations a lot easier and more reliable. Checking if two double values are equal is often not reliable.


There are a lot of ways to structure this kind of exercise. Below I have posted one version. I'm not claiming it to be the way to do it, but you may find some inspiration.

The approach is rather traditional, and I have focused on an easy-to-follow workflow, naming and testability - at least to a some level. It's only using integer values and calculations.

The solver itself controlling the workflow and doing the calculations:

/// <summary>
/// Creates a random linear system of two equations with two variables of first degree and find a
/// possible solution with integral values for the variables x and y. This is the same as
/// finding the intersection between two lines in the plane.
/// </summary>
public class LinearSystemSolver
{
  private readonly int maxRetries;
  private readonly ICoefficientProvider coefficientProvider;

  /// <summary>
  /// Constructor
  /// </summary>
  /// <param name="maxRetries">The number of times to try to find a valid solution before returning false from TrySovle().</param>
  /// <param name="coefficientProvider">An object that provides the coefficients in a linear system to solve.</param>
  public LinearSystemSolver(int maxRetries, ICoefficientProvider coefficientProvider)
  {
    this.maxRetries = maxRetries;
    this.coefficientProvider = coefficientProvider
                               ?? throw new ArgumentNullException(nameof(coefficientProvider));
  }

  public bool TrySolve(out IntegralLinearSystem result)
  {
    result = IntegralLinearSystem.Empty;

    for (int i = 0; i < maxRetries; i++)
    {
      Coefficients coefficients = GenerateCoefficients();
      if (HasSolution(coefficients) && GetIntegralSolution(coefficients, out int x, out int y))
      {
        result = new IntegralLinearSystem(coefficients, x, y);
        return true;
      }
    }

    return false;
  }

  private Coefficients GenerateCoefficients()
  {
    return coefficientProvider.GetCoefficients();
  }

  private bool HasSolution(Coefficients coefficients)
  {
    // This is a test of bd - ae != 0
    return coefficients.B * coefficients.D != coefficients.A * coefficients.E;
  }

  private bool GetIntegralSolution(Coefficients coefficients, out int x, out int y)
  {
    int numY = coefficients.C * coefficients.D - coefficients.A * coefficients.F;
    int denomY = coefficients.B * coefficients.D - coefficients.A * coefficients.E;
    y = numY / denomY;

    int numX;
    int denomX;
    if (coefficients.A != 0)
    {
      numX = coefficients.C - coefficients.B * y;
      denomX = coefficients.A;
    }
    else
    {
      // A and D can not both be 0 because the system then doesn't have a solution (the two lines are parallel with the x-axis)
      // A = D = 0 would have been caught by HasSolution() before ending here
      numX = coefficients.F - coefficients.E * y;
      denomX = coefficients.D;
    }
    x = numX / denomX;

    return numY % denomY == 0 && numX % denomX == 0;
  }
}

An object representing the integral solution - if found:

/// <summary>
/// Object representing a linear system of first degree with two variables with integral solutions for x and y.
/// </summary>
public struct IntegralLinearSystem
{
  public static readonly IntegralLinearSystem Empty = new IntegralLinearSystem(Coefficients.Empty, 0, 0);

  public IntegralLinearSystem(Coefficients coefficients, int x, int y)
  {
    Coefficients = coefficients;
    X = x;
    Y = y;

    if (!IsValid)
      throw new InvalidOperationException("Inconsistent integral linear system");
  }

  public readonly Coefficients Coefficients;
  public readonly int X;
  public readonly int Y;

  public bool IsValid
  {
    get
    {
      return
        Coefficients.A * X + Coefficients.B * Y == Coefficients.C &&
        Coefficients.D * X + Coefficients.E * Y == Coefficients.F;
    }
  }

  public override string ToString()
  {
    StringBuilder builder = new StringBuilder();
    builder.Append(Coefficients);
    builder.AppendLine();
    builder.AppendLine("<=>");
    builder.AppendFormat("x = {0}, y = {1}", X, Y);

    return builder.ToString();
  }
}

An object representing the constant values (coefficients) for the system

/// <summary>
/// Object holding the coefficients to x and y and values on the right side 
/// of the equal sign in a linear system with two variables of first degree
/// on the form <code>ax + by = c</code> and <code>dx + ey = f</code>
/// </summary>
public struct Coefficients
{
  public static readonly Coefficients Empty = new Coefficients();

  public readonly int A;
  public readonly int B;
  public readonly int C;
  public readonly int D;
  public readonly int E;
  public readonly int F;

  public Coefficients(int a, int b, int c, int d, int e, int f)
  {
    A = a;
    B = b;
    C = c;
    D = d;
    E = e;
    F = f;
  }

  public override string ToString()
  {
    return $"{A}x + {B}y = {C}{Environment.NewLine}{D}x + {E}y = {F}";
  }
}

The contract between the solver and the generator of the constant values:

public interface ICoefficientProvider
{
  Coefficients GetCoefficients();
}

An object providing randomly generated coefficients to the system:

/// <summary>
/// Creates randomly generated coefficients to a linear system of two variables.
/// </summary>
public class RandomCoefficientProvider : ICoefficientProvider
{
  private readonly int min;
  private readonly int max;
  private readonly Random random;

  /// <summary>
  /// Constructor
  /// </summary>
  /// <param name="min">The inclusive lower boundary of the random numbers returned.</param>
  /// <param name="max">The exclusive upper boundary of the random numbers returned.</param>
  /// <param name="randomSeed"></param>
  public RandomCoefficientProvider(int min, int max, int? randomSeed = null)
  {
    this.min = Math.Min(min, max);
    this.max = Math.Max(min, max);
    random = randomSeed == null ? new Random() : new Random(randomSeed.Value);
  }

  public Coefficients GetCoefficients()
  {
    return new Coefficients
    (
      random.Next(min, max),
      random.Next(min, max),
      random.Next(min, max),
      random.Next(min, max),
      random.Next(min, max),
      random.Next(min, max)
    );
  }
}

Use Case:

  LinearSystemSolver solver = new LinearSystemSolver(10, new RandomCoefficientProvider(1, 10));
  if (solver.TrySolve(out IntegralLinearSystem result))
  {
    Console.WriteLine($"Result:{Environment.NewLine}{result}");
  }
  else
  {
    Console.WriteLine("No integral solution found");
  }

As seen, I have created the interface ICoefficientProvider in order to separate the (random) creation of the constant values of the system from the workflow and calculations. It gives me the possibility to implement another coefficient provider like the below - for testing purposes - without changing anything in the solver:

/// <summary>
/// A Coefficient Provider that can be used for testing purposes
/// </summary>
public class ConstantCoefficientProvider : ICoefficientProvider
{
  private readonly Coefficients coefficients;

  public ConstantCoefficientProvider(int a, int b, int c, int d, int e, int f)
  {
    coefficients = new Coefficients(a, b, c, d, e, f);
  }

  public Coefficients GetCoefficients()
  {
    return coefficients;
  }
}

You could easily extend this pattern of dependency injection to handle other parts of the program, for instance the calculations.

\$\endgroup\$

Not the answer you're looking for? Browse other questions tagged or ask your own question.