Formatting Terms and Operations

I mentioned in my last post that the logic for displaying terms and operations was much more complicated than I originally anticipated.  The logic evolved as the game development progressed, and CJ and I identified new and interesting edge cases to be addressed.

I’ll begin with the logic behind the operations.  Both ConstantOperation and VariableOperation have their ToString() methods overridden.  ConstantOperation is the simpler of the two:

public override string ToString()
{
    return Utility.ToConstantOperationString(this.Numerator, this.Denominator, this.Operand);
}

The ToConstantOperationString method does all of the heaving lifting here:

public static string ToConstantOperationString(int Numerator, int Denominator, Operands Operand)
{
    string OperandSymbol, Sign;
    OperandSymbol = "";
    Sign = "";
    switch (Operand)
    {
        case Operands.Add:
            OperandSymbol = ((float)Numerator / (float)Denominator < 0 ? "-" : "+");
            Sign = "";
            break;
        case Operands.Subtract:
            OperandSymbol = ((float)Numerator / (float)Denominator < 0 ? "+" : "-");
            Sign = "";
             break;
        case Operands.Multiply:
            OperandSymbol = "*";
            Sign = ((float)Numerator / (float)Denominator < 0 ? "-" : "");
            break;
        case Operands.Divide:
            OperandSymbol = "/";
            Sign = ((float)Numerator / (float)Denominator < 0 ? "-" : "");
            break;
    }

    return string.Format("{0}{1}{2}{3}{4}{5}{6}",
                  OperandSymbol,
                 Sign,
                  (Math.Abs(Denominator) == 1 ? "" : "("),
                 Math.Abs(Numerator),
                 (Math.Abs(Denominator) == 1 ? "" : "/"),
                 (Math.Abs(Denominator) == 1 ? "" : Math.Abs(Denominator).ToString()),
                 (Math.Abs(Denominator) == 1 ? "" : ")"));
}

The switch statement sorts out which operand and which sign will be displayed.  The latter is dependent on the former.  For adds & subtracts, the operand and the sign end up effectively merged with each other.

  • Adding a positive 4: "+4"
  • Adding a negative 4: "-4"
  • Subtracting a positive 4: "-4"
  • Subtracting a negative 4: "+4"

Multiplication and division, however, preserve both the operand and the sign (at least when the number is negative):

  • Multiplying a positive 4: "*4"
  • Multiplying a negative 4: "*-4"
  • Dividing by a positive 4: "/4"
  • Dividing by a negative 4: "/-4"

Then we need to format the final value, which includes the operand, the sign, and the number itself.  Whole numbers like the above examples are straightforward.  Fractional numbers present an additional challenge.  I made the decision to surround fractional operations in parentheses, to set the absolute value apart from the sign, operand, and (as we’ll see later this post), the variable letter:

  • +(4/3)
  • -(4/3)
  • /-(4/3)
  • *-(4/3)

As the Format() function at the end suggests, each constant is made up of 7 pieces:

  • The operand itself (+, -, *, or /)
  • The sign of the constant
  • An open parenthesis, if this is a fraction (denoted by a non-1 denominator)
  • The numerator
  • A slash, if this is a fraction
  • The denominator, if this is a fraction
  • A closing parenthesis, if this is a fraction

It is also important to note that the numerator, the denominator, or both, may be negative.  However, it all cases, the presentation of that value will never be something like "-4/-3".  The signs of the two components effective get evaluated in the expression:

((float)Numerator / (float)Denominator < 0 ? … : … )

VariableOperation.ToString() use the exact same logic for coming up with the coefficient, but includes two additional pieces of logic:

public override string ToString()
{
    String InitialCoefficientString;

    InitialCoefficientString = Utility.ToConstantOperationString(this.Numerator, this.Denominator, this.Operand);
    if(Math.Abs(this.Numerator) == 1 && Math.Abs(this.Denominator) == 1) { InitialCoefficientString = InitialCoefficientString.Replace("1", ""); }

    return string.Format("{0}{1}", InitialCoefficientString, this.Var);
}

First, it appends the variable letter itself, in the Format() call.  Second, it evaluates the coefficient to see if it is exact "1".  If so, it drops the coefficient completely, so that the player sees "+x" rather than "+1x", by simply replacing the "1" with an empty string.  (However, since this version of the game does not allow you to "multiply by x" or "divide by x", the player wouldn’t see something like "*x" or "/x" anyway, so this bit of logic is really just future-proofing.)

That’s all there is for rendering operations.  Let’s move on to how terms are presented. 

***

Terms have a slightly different set of concerns than operations.  The operations have to show one of four operands, every time, while terms only have to worry about rendering "+" and "-".  In some cases, an operand is not shown at all, as in the case of leading, positive values:

4x – 1 = 15

This concept of "leading term" crops up a couple of times in the other methods we’ve looked at.  The signs for the first term on either side of the equal sign get treated a little differently.

As with the operations, the Constant and Variable classes have their ToString() methods overridden.  Let’s start with Constant:

public override string ToString()
{
    if(this.Numerator == 0) { return "0"; }

    return string.Format("{0}{1}{2}{3}",
                         (((float)this.Numerator / (float)this.Denominator) > 0f ? (this.IsFirstOnThisSide ? "" : "+ ") : (this.IsFirstOnThisSide ? "-" : "- ")),
                         Math.Abs(this.Numerator),
                          (Math.Abs(this.Denominator) == 1 ? "" : "/"),
                         (Math.Abs(this.Denominator) == 1 ? "" : Math.Abs(this.Denominator).ToString()));
}

Much of this should look familiar from ConstantOperation.  The notable exceptions being the lack of parentheses, and the presentation of the sign.  The latter gets subtly modified depending on whether this is the first term in the equation or not.  First-term additions show no sign:

4 + 3x = 2

While other additions show a "+" followed by a space.

3x + 4 = 2

The space improves the look of the equation.  Without it, it would look like

3x +4 = 2

First-term subtractions show the minus sign with no intervening space (the "-3" here), but other subtractions (the "- 9" here) include the space:

4x = -3 – 9

****

Now let’s look at Variable terms.  The Variable version of ToString() combines these the approaches:

public override string ToString()
{
    return string.Format("{0}{1}{2}{3}{4}{5}{6}",
                         (((float)this.Numerator / (float)this.Denominator) > 0f ? (this.IsFirstOnThisSide ? "" : "+ ") : (this.IsFirstOnThisSide ? "-" : "- ")),
                          (Math.Abs(this.Denominator) == 1 ? "" : "("),
                          (Math.Abs(this.Numerator) == 1 && Math.Abs(this.Denominator) == 1 ? "" : Math.Abs(this.Numerator).ToString()),
                          (Math.Abs(this.Denominator) == 1 ? "" : "/"),
                         (Math.Abs(this.Denominator) == 1 ? "" : Math.Abs(this.Denominator).ToString()),
                          (Math.Abs(this.Denominator) == 1 ? "" : ")"),
                          this.Var);
}

Parentheses are included (to distinguish the sign, coefficient, and variable more clearly), as is the "first term" logic.

***

At this point, you might be asking why I ended up with three different approaches to formatting the values.  The requirements for operations, constant terms, and variable terms have overlap, but none of them were complete subsets of the others.  That makes it trickier to factor anything out.  I think you can make the argument, though, that showing a constant operation of "-(3/4)", and a constant term of "-3/4" (as the game currently does) is inconsistent and should be normalized.  Doing so would go a long way to making it easier to refactor the logic together.  Another improvement for a later version.

This concludes the series on the algebra game.  There are still plenty of things that we’d like to do with the game before we consider submitting it to one or both app stores: replace the temporary graphics, add usage tracking, add error logging, and so on.  I’ve had a blast building it to this point and (for the most part) loved the discussions with CJ about its direction.  (We won’t speak of the "I’m sorry dear; I really think you need to represent numbers as fractions" conversation again.)

Advertisement