The fluent calculator kata – Rev 2

In my last post I described the fluent calculator kata which we came up with for our coding dojo. When we started implementing the kata we decided to modify the initial set of  “requirements” slightly in order to make it a bit more complex. Here is the changed requirement:

  • The calculator should never throw exceptions. When an overflow occurs the calculator should throw an InvalidOperationException with an inner ArithmeticException.

I’ll show you what design and tests we came up with.

The tests we came up with during the dojo are:

    using System;

    using FluentAssertions;

    using NUnit.Framework;

    public class CalculatorTest
    {
        [Test]
        public void Calc_ShouldReturnSeed()
        {
            int result = Calculator.Calc(10);

            result.Should().Be(10);
        }

        [Test]
        public void Plus_ShouldReturnSum()
        {
            int result = Calculator.Calc(10).Plus(5);

            result.Should().Be(15);
        }

        [Test]
        public void PlusWithNegativeValue_ShouldReturnSum()
        {
            int result = Calculator.Calc(10).Plus(-10);

            result.Should().Be(0);
        }

        [Test]
        public void Plus_WithMaxInt_ShouldThrowInvalidOperationExceptionWithInnerArithmeticException()
        {
            Action act = () => Calculator.Calc(1).Plus(int.MaxValue);

            act.ShouldThrow<InvalidOperationException>().WithInnerException<ArithmeticException>();
        }

        [Test]
        public void Plus_3Times_ShouldReturnSum()
        {
            int result = Calculator.Calc(10).Plus(1).Plus(2).Plus(3);

            result.Should().Be(16);
        }

        [Test]
        public void Minus_3Times_ShouldReturnSum()
        {
            int result = Calculator.Calc(10).Minus(1).Minus(2).Minus(3);

            result.Should().Be(4);
        }

        [Test]
        public void Minus_ShouldReturnDifference()
        {
            int result = Calculator.Calc(10).Minus(5);

            result.Should().Be(5);
        }

        [Test]
        public void MinusTwo_WithMaxInt_ShouldThrowInvalidOperationExceptionWithInnerArithmeticException()
        {
            Action act = () => Calculator.Calc(-2).Minus(int.MaxValue);

            act.ShouldThrow<InvalidOperationException>().WithInnerException<ArithmeticException>();
        }

        [Test]
        public void MinusOne_WithMaxInt_ShouldBeMinValue()
        {
            int result = Calculator.Calc(-1).Minus(int.MaxValue);

            result.Should().Be(int.MinValue);
        }

        [Test]
        public void MinusMinInt_WithMinInt_ShouldBeZero()
        {
            int result = Calculator.Calc(int.MinValue).Minus(int.MinValue);

            result.Should().Be(0);
        }

        [Test]
        public void MinusWithNegativeValue_ShouldReturnSum()
        {
            int result = Calculator.Calc(10).Minus(-10);

            result.Should().Be(20);
        }

        [Test]
        public void UndoWithPlus_ShouldUndoLastOperation()
        {
            int result = Calculator.Calc(10).Plus(5).Undo();

            result.Should().Be(10);
        }

        [Test]
        public void UndoWith3Plus_ShouldUndoOperations()
        {
            int result = Calculator.Calc(10).Plus(1).Plus(2).Plus(3).Undo().Undo();

            result.Should().Be(11);
        }

        [Test]
        public void UndoMixedWithPlus_ShouldUndoLastOperation()
        {
            int result = Calculator.Calc(10).Plus(1).Plus(2).Undo().Plus(3).Undo().Undo();

            result.Should().Be(10);
        }

        [Test]
        public void Undo_AfterCalc_ShouldReturnSeed()
        {
            int result = Calculator.Calc(10).Undo();

            result.Should().Be(10);
        }

        [Test]
        public void Undo_AfterCalcMultipleTimes_ShouldReturnSeed()
        {
            int result = Calculator.Calc(10).Undo().Undo();

            result.Should().Be(10);
        }

        [Test]
        public void UndoWithMinus_ShouldUndoLastOperation()
        {
            int result = Calculator.Calc(10).Minus(5).Undo();

            result.Should().Be(10);
        }

        [Test]
        public void RedoWithPlus_ShouldRedoLastOperation()
        {
            int result = Calculator.Calc(10).Plus(5).Undo().Redo();

            result.Should().Be(15);
        }

        [Test]
        public void Redo_AfterCalc_ShouldReturnSeed()
        {
            int result = Calculator.Calc(10).Undo().Redo();

            result.Should().Be(10);
        }

        [Test]
        public void Redo_AfterCalcWithoutUndo_ShouldReturnSeed()
        {
            int result = Calculator.Calc(10).Redo();

            result.Should().Be(10);
        }

        [Test]
        public void RedoWithMinus_ShouldRedoLastOperation()
        {
            int result = Calculator.Calc(10).Minus(5).Undo().Redo();

            result.Should().Be(5);
        }

        [Test]
        public void RedoWithPlus_WithoutUndo_ShouldReturnPrevious()
        {
            int result = Calculator.Calc(10).Plus(5).Redo();

            result.Should().Be(15);
        }
    }

and here is the calculator code:

    public class Calculator
    {
        private readonly Calculator previousCalculator;

        private readonly int result;

        private Calculator nextCalculator;

        private Calculator(int result)
        {
            this.result = result;
        }

        private Calculator(Calculator previousCalculator, int seed) : this(seed)
        {
            this.previousCalculator = previousCalculator;

        }

        public static Calculator Calc(int seed)
        {
            return new Calculator(seed);
        }

        public Calculator Plus(int value)
        {
            checked
            {
                try
                {
                    this.nextCalculator = new Calculator(this,this.result + value);
                    return this.nextCalculator;
                }
                catch (ArithmeticException e)
                {
                    throw new InvalidOperationException(e.Message, e);
                }
            }
        }

        public Calculator Minus(int value)
        {
            checked
            {
                try
                {
                    this.nextCalculator = new Calculator(this, this.result - value);
                    return this.nextCalculator;
                }
                catch (ArithmeticException e)
                {
                    throw new InvalidOperationException(e.Message, e);
                }
            }
        }

        public Calculator Undo()
        {
            return this.previousCalculator ?? this;
        }

        public Calculator Redo()
        {
            return this.nextCalculator ?? this;
        }

        public static implicit operator int(Calculator calculator)
        {
            return calculator.result;
        }
    }

Notice the approach we took to design the calculator. The constructor of the calculator is private. There are actually two constructors available. The first constructor takes in the seed to start the calculation. The second constructor takes in another calculator and the seed of the current calculations. The calculator internally has two fields which reference the previous and the next calculator in the chain. Practically this creates a linked list of calculators which allow to move forward or backward in the calculator chain. The boundary conditions for undo and redo simply check whether the previous calculator is null (in the undo case) or the next calculator is null (in the redo case) and if this is true return the current calculator (this).

Florian Scheiwiler sent me his solution to the initial set of requirements. Let us see how he approached it:

    [TestFixture]
    public class Test
    {
        [Test]
        public void Sample1()
        {
            Assert.AreEqual(20, (int)new Calculator()
                .Calc(10)
                .Plus(5)
                .Minus(2)
                .Undo()
                .Redo()
                .Undo()
                .Plus(5));
        }

        [Test]
        public void Sample2()
        {
            Assert.AreEqual(13, (int)new Calculator()
                .Calc(10)
                .Plus(5)
                .Minus(2)
                .Undo()
                .Undo()
                .Redo()
                .Redo()
                .Redo());
        }

        [Test]
        public void Sample3()
        {
            Assert.AreEqual(18, (int)new Calculator()
                .Calc(10)
                .Plus(5)
                .Minus(2)
                .Save()
                .Undo()
                .Redo()
                .Undo()
                .Plus(5));
        }
    }

and the calculator

    public class Calculator
    {
        private Stack<Func<int, int>> _actions;
        private Stack<Func<int, int>> _undone;

        public Calculator Calc(int n)
        {
            Init(n);
            return this;
        }

        private void Init(int n)
        {
            ResetActionList();
            Plus(n);
            ResetUndoList();
        }

        public Calculator Plus(int n)
        {
            _actions.Push((a) => a + n);
            return this;
        }

        public Calculator Minus(int n)
        {
            _actions.Push((a) => a - n);
            return this;
        }

        public Calculator Undo()
        {
            if (_actions.Count > 1) // cannot undo first init action!
            {
                _undone.Push(_actions.Pop());

            }
            return this;
        }

        public Calculator Redo()
        {
            if (_undone.Any())
            {
                _actions.Push(_undone.Pop());
            }
            return this;
        }

        public Calculator Save()
        {
            Init(Result());
            return this;
        }

        private int Result()
        {
            return _actions.Aggregate(0, (current, f) => f(current));
        }

        public static explicit operator int(Calculator c)
        {
            return c.Result();
        }

        private void ResetActionList()
        {
            _actions = new Stack<Func<int, int>>();
        }

        private void ResetUndoList()
        {
            _undone = new Stack<Func<int, int>>();
        }
    }
}

Thanks Flo for sharing this code! Maybe he’ll also rewrite it with the changed requirements. I think that’s going to be tricky with function delegates 😀

About the author

Daniel Marbach

1 comment

Recent Posts