This is a test task for one of the .net developer vacancies. I don't publish the task definition as I think they wouldn't be happy with that.
- Test.Calculator - a library
- Test.Calculator.Tests - the unit tests (MSTest)
- Test.Calculator.App - a test app
- All public methods of the library are documented
- Null safety checked correctly
- Exceptions are handled
- Unit tests are written
- Negative tests exist
- Edge values of
double
are handled - Exceptions thrown have meaningful messages
- A user is able to extend the library, an example provided
- All tests given in the task definition are green, see
Test.Calculator.Tests.MainTestsGiven
- One known bug is documented
I've decided to go with the lazy calculation, because otherwise I would have to do CPU-heavy work in the constructors, which is generally not recommended.
Although you the required method names were .toResult()
, .print()
, .printSentence()
, I've decided to go with .ToResult()
, .Print()
, .PrintSentence()
to align with the recommended naming conventions for .net.
It was required to use the double
type. In this type, the division by zero and other is a valid operation that returns double.PositiveInfinity
. So I thought since the return type is a double
, returning the double.PositiveInfinity
would be valid.
However, I do handle the subnormal results and other corner cases in the .PrintSentence()
correctly, since that operation returns a readable message intended for people.
The cases handled are:
- positive infinity
- negative infinity
- not a number (NaN)
- too small number (less than the
double.Epsilon
) - too large number (larger than the
double
range)
The .PrintSentence()
returns a meaningful message for them.
Since the Fraction
is printed in the .PrintSentence()
as x/y, it would not be possible to do print it correctly if it accepted other Operations as parameters. And it would make it almost indistinguishable from the Division
.
So I assumed the goal of the Fraction
is to represent a numeric constant in a fraction format, mainly to display it on printing in a readable way.
Which led me to a decision to not accept other operations in the Fraction
operation.
I introduced a Constant
operation for one reason:
- It is convenient for other operations to process their operands regardless whether they are numerics or nodes. It allowed me to save lots of code lines.
It also allowed me to have a nice bonus:
Sum
andMultiplication
operations now support more than two operands.
However, there is one drawback to that:
- I had to introduce the
OperationBase.AddParenthesesOnPrinting
virtual property that istrue
for all operations but overriden tofalse
in theConstant
.
I've taken care of the consumers to allow them extend the library, and I have provided an example of that: Test.Calculator.App.Logarithm
.
Passing the appendChild
action parameter to the abstract OperationBase.AppendSentence
and OperationBase.AppendMath
methods
Passing it as a parameter is not a beautiful way.
However I did not want to make AppendMathWithParentheses
and AppendSentence
public (those are right now private), since they are only needed for the internal use in the library.
And since I intended to make the library extensible, I could not make those methods protected
or protected internal
either (it would not work: since the OperationBase
is a universal type for the arguments, the children wouldn't be able to call those).
So I decided to hide the implementation details from the users of the library and pass this recursive action as a delegate.
I do not handle slight precision errors, for example:
((5.6 + 5.8) - 0.4) = 10.999999999999998
It leads to the wrong Faculty
behavior:
Console.WriteLine(new Faculty(5.6d + 5.8d - 0.4d).PrintSentence());
faculty of 10.999999999999998 could not be calculated precisely. A faculty of a non-integer 10.999999999999998 could not be calculated, only whole numbers are supported.
I thought since one of the requirements were to use a double, I should rely on that type, and trying to handle those correctly would cause incorrect handling of other valid cases. For example:
new Division(new Subtraction(1000000000000000, .2d), 100000000000000).Print()
;((1000000000000000 - 0.2) / 100000000000000) = 9.999999999999998
Which is the correct behavior. To handle both correctly would be quite complex, so I've decided to skip that and just document it.
The output of the Test.Calculator.App:
A normal expression example:
(2 + (((6 / 2) * 2)!)) = 722
sum of 2 and faculty of multiplication of division of 6 by 2 and 2 is 722
A subnormal result example:
(2 + (3 / 0)) = ∞
sum of 2 and division of 3 by 0 could not be calculated precisely, the result value is too high. A division by zero is probable.
An exception example:
(((5 / 2) * 2)!) = 120
faculty of multiplication of division of 5 by 2 and 2 is 120
A known bug example:
((5.6 + 5.8) - 0.4) = 10.999999999999998
sum of 5.6 and 5.8 minus 0.4 is 10.999999999999998
A custom operation example:
(3 + (9 log (6/2))) = 5
sum of 3 and logarithm of 9 to the base 6/2 is 5
- Clone the repository
- Open it in the Visual Studio
- Run the tests or run the
Test.Calculator.App
app - Extend those to check your own expressions
Normally, I would publish the library as a nuget package, but it's just a test task, so:
- Copy the
Test.Calculator
andTest.Calculator.Tests
to your solution - Reference and use them
Thank you for taking your time reading this and have a good day :)