10
\$\begingroup\$

I am working on an endless tower defense game in Unity. To keep track of money, damage, hitpoints and stuff like that I will be using BigInteger, which can represent arbitrarily large integer values. Unfortunately, Unity does not come with a BigInteger implementation, but I managed to find one online.

For displaying a BigInteger on the screen, I have created a BigIntegerFormatter interface and three implementations of this interface:

  • ScientificFormatter: turns 1000000 into 1.000e6
  • NumericalFormatter: turns 1000000 into 1.000M, following suffixes are B(illion), T(rillion), Q(uadrillion) and after that a, b, c, d, e and so on. When 1000z is reached, the number will be formatted to 1.000aa. For further details on the specifics of this formatter, look at the unittests
  • CachedFormatter: Due to the computational complexity of the BigInteger.ToString() method, I have created this class, which keeps a Dictionary of BigIntegers and their corresponding formatted string.

Please take into consideration that I have extremely little experience programming in C# (the code below is probably more than 80% of the C# programming I've ever done). So pointers on C# best practices and so on are very welcome. Besides code readability and stuff like that, performance improvements are also welcome feedback!

Interface

public interface BigIntegerFormatter  {
    /// <summary>
    /// Format a BigInteger number to a smaller, more readable notation.
    /// </summary>
    /// <returns></returns>
    string Format(BigInteger number);
}

Implementations

public class ScientificFormatter : BigIntegerFormatter
{
    public string Format(BigInteger number)
    {
        return FormatNumberString(number.ToString());
    }

    private string FormatNumberString(string numberString)
    {
        if (numberString.Length < 4)
        {
            return numberString;
        }

        int exponent = numberString.Length - 1;
        string leadingDigit = numberString.Substring(0, 1);
        string decimals = numberString.Substring(1, 3);

        return String.Format("{0}.{1}e{2}", leadingDigit, decimals, exponent);
    }
}

Please note that I got some help with the class below from this question.

public class NumericalFormatter : BigIntegerFormatter
{
    private const int PRECOMPUTE_FOUR_CHARACTERS = 531441; // 27 ^ 4
    private static List<string> preComputedBase27Values = preComputeBase27Values();

    private static List<string> preComputeBase27Values()
    {
        List<string> preComputedValues = new List<string>();

        for (int i = 0; i < PRECOMPUTE_FOUR_CHARACTERS; i++)
        {
            string text = ToBase27AlphaString(i);

            if (!text.Contains('`'))
            {
                preComputedValues.Add(text);
            }
        }

        return preComputedValues;
    }

    public string Format(BigInteger number)
    {
        return FormatNumberString(number.ToString());
    }

    private static string FormatNumberString(string number)
    {
        if (number.Length < 5)
        {
            return number;
        }

        if (number.Length < 7)
        {
            return FormatThousands(number);
        }

        return FormatGeneral(number);
    }

    private static string FormatThousands(string number)
    {
        string leadingNumbers = number.Substring(0, number.Length - 3);
        string decimals = number.Substring(number.Length - 3);

        return CreateNumericalFormat(leadingNumbers, decimals, "K");
    }

    private static string CreateNumericalFormat(string leadingNumbers, string decimals, string suffix)
    {
        return String.Format("{0}.{1}{2}", leadingNumbers, decimals, suffix);
    }

    private static string FormatGeneral(string number)
    {
        int amountOfLeadingNumbers = (number.Length - 7) % 3 + 1;
        string leadingNumbers = number.Substring(0, amountOfLeadingNumbers);
        string decimals = number.Substring(amountOfLeadingNumbers, 3);

        return CreateNumericalFormat(leadingNumbers, decimals, GetSuffixForNumber(number));
    }

    private static string GetSuffixForNumber(string number)
    {
        int numberOfThousands = (number.Length - 1) / 3;

        switch (numberOfThousands)
        {
            case 1:
                return "K";
            case 2:
                return "M";
            case 3:
                return "B";
            case 4:
                return "T";
            case 5:
                return "Q";
            default:
                return GetProceduralSuffix(numberOfThousands - 5);
        }
    }

    private static string GetProceduralSuffix(int numberOfThousandsAfterQ)
    {
        return preComputedBase27Values[numberOfThousandsAfterQ - 1];
    }

    private static string ToBase27AlphaString(int value)
    {
        return ToBaseNAlphaString(value, '`', 27);
    }

    private static string ToBaseNAlphaString(int value, char baseChar, int numericBase)
    {
        StringBuilder sb = new StringBuilder();

        while (value > 0)
        {
            int digit = value % numericBase;

            sb.Append((char)(baseChar + digit));
            value /= numericBase;
        }

        if (sb.Length == 0)
        {
            sb.Append(baseChar);
        }

        sb.Reverse();
        return sb.ToString();
    }
}

internal static class Extensions
{
    public static void Reverse(this StringBuilder sb)
    {
        for (int i = 0, j = sb.Length - 1; i < sb.Length / 2; i++, j--)
        {
            char chT = sb[i];

            sb[i] = sb[j];
            sb[j] = chT;
        }
    }
}

The CachedFormatter takes a BigIntegerFormatter as a constructor argument and then caches its results.

public class CachedFormatter : BigIntegerFormatter
{
    BigIntegerFormatter bigIntegerFormatter;
    Dictionary<BigInteger, string> numberCache = new Dictionary<BigInteger, string>();
    LinkedList<BigInteger> listOfCachedNumbers = new LinkedList<BigInteger>();

    int maxCacheSize;

    public CachedFormatter(BigIntegerFormatter bigIntegerFormatter, int maxCacheSize)
    {
        this.bigIntegerFormatter = bigIntegerFormatter;
        this.maxCacheSize = maxCacheSize;
    }

    public string Format(BigInteger number)
    {
        try
        {
            return FromCache(number);
        }
        catch (KeyNotFoundException)
        {
            string formattedNumber = bigIntegerFormatter.Format(number);
            AddToCache(number, formattedNumber);

            return formattedNumber;
        }
    }

    private string FromCache(BigInteger number)
    {
        return numberCache[number];
    }

    private void AddToCache(BigInteger number, string formattedNumber)
    {
        listOfCachedNumbers.AddLast(number);
        numberCache.Add(number, formattedNumber);

        if (this.numberCache.Count() > this.maxCacheSize)
        {
            RemoveOldestNumberFromCache();
        }
    }

    private void RemoveOldestNumberFromCache()
    {
        BigInteger oldestNumber = listOfCachedNumbers.First();

        listOfCachedNumbers.RemoveFirst();
        numberCache.Remove(oldestNumber);
    }
}

Testing

Note: Some of these tests contain very long strings of numbers and scroll to the right for a pretty long time!

First of all, since I had taken the BigInteger class somwhere from the internet I created a few test to ensure some of its functionality:

namespace UnitTestProject.BigIntegerTest
{
    [TestClass]
    public class BigIntegerTest
    {
        [TestMethod]
        public void TestAddition()
        {
            foreach (string[] data in DpAddition())
            {
                BigInteger leftOperand = new BigInteger(data[0]);
                BigInteger rightOperand = new BigInteger(data[1]);
                string expectedOutcome = data[2];

                Assert.AreEqual(expectedOutcome, (leftOperand + rightOperand).ToString());
            }
        }

        public string[][] DpAddition()
        {
            return new string[][]
            {
                new string[] { "0", "0", "0" },
                new string[] { "1", "0", "1" },
                new string[] { "-1", "0", "-1" },
                new string[] { "-1", "-1", "-2" },
                new string[] { "-100", "100", "0" },
                new string[] { "-343334524352343523452345", "-12334123341234132412341234", "-12677457865586475935793579" },
                new string[] { "343334524352343523452345", "12334123341234132412341234", "12677457865586475935793579" },
            };
        }

        [TestMethod]
        public void TestSubtraction()
        {
            foreach (string[] data in DpSubtraction())
            {
                BigInteger leftOperand = new BigInteger(data[0]);
                BigInteger rightOperand = new BigInteger(data[1]);
                string expectedOutcome = data[2];

                Assert.AreEqual(expectedOutcome, (leftOperand - rightOperand).ToString());
            }
        }

        public string[][] DpSubtraction()
        {
            return new string[][]
            {
                new string[] { "0", "0", "0" },
                new string[] { "1", "0", "1" },
                new string[] { "-1", "0", "-1" },
                new string[] { "-1", "-1", "0" },
                new string[] { "-100", "100", "-200" },
                new string[] { "132412341234123412341534252345", "123412341234153345235", "132412341110711071107380907110" },
                new string[] { "123412341234153345235", "132412341234123412341534252345", "-132412341110711071107380907110" },
            };
        }

        [TestMethod]
        public void TestMultiplication()
        {
            foreach (string[] data in DpMultiplication())
            {
                BigInteger leftOperand = new BigInteger(data[0]);
                BigInteger rightOperand = new BigInteger(data[1]);
                string expectedOutcome = data[2];

                Assert.AreEqual(expectedOutcome, (leftOperand * rightOperand).ToString());
            }
        }

        public string[][] DpMultiplication()
        {
            return new string[][]
            {
                new string[] { "0", "0", "0" },
                new string[] { "1", "0", "0" },
                new string[] { "-1", "0", "0" },
                new string[] { "-1", "-1", "1" },
                new string[] { "-5", "2", "-10" },
                new string[] { "-4", "3", "-12" },
                new string[] { "-10", "-0", "0" },
                new string[] { "10", "-0", "0" },
                new string[] { "-100", "100", "-10000" },
                new string[] { "132412341234123412341534252345", "123412341234153345235", "16341317039998792050345358946184719163916393326075" },
            };
        }

        [TestMethod]
        public void TestNoRoundingDivision()
        {
            foreach (string[] data in DpNoRoundingDivision())
            {
                BigInteger leftOperand = new BigInteger(data[0]);
                BigInteger rightOperand = new BigInteger(data[1]);
                string expectedOutcome = data[2];

                Assert.AreEqual(expectedOutcome, (leftOperand / rightOperand).ToString());
            }
        }

        public string[][] DpNoRoundingDivision()
        {
            return new string[][]
            {
                new string[] { "-1", "-1", "1" },
                new string[] { "-4", "3", "-1" },
                new string[] { "-100", "100", "-1" },
                new string[] { "132412341224554568032863990160", "123412341234153345235", "1072926256" }, // Exact outcome, no rounding involved
            };
        }

        [TestMethod]
        public void TestRoundingDivisionCutsOff()
        {
            foreach (string[] data in DpRoundingDivision())
            {
                BigInteger leftOperand = new BigInteger(data[0]);
                BigInteger rightOperand = new BigInteger(data[1]);
                string expectedOutcome = data[2];

                Assert.AreEqual(expectedOutcome, (leftOperand / rightOperand).ToString());
            }
        }

        public string[][] DpRoundingDivision()
        {
            return new string[][]
            {
                new string[] { "100", "6", "16" },
                new string[] { "100", "7", "14" },
                new string[] { "100", "12", "8" },
                new string[] { "100", "17", "5" },
                new string[] { "987343509823458976324034509872345097243059", "123412341234153345236", "8000362848235309403197" },
            };
        }

        [TestMethod]
        [ExpectedException(typeof(DivideByZeroException))]
        public void TestDivisionByZeroThrowsException()
        {
            BigInteger divisionByZero = new BigInteger("1") / new BigInteger("0");
        }

        [TestMethod]
        public void TestBigIterativeMultiplication()
        {
            BigInteger number = new BigInteger("4562345724335");
            BigInteger otherNumber = new BigInteger("521352345232153435");

            for (int i = 0; i < 22; i++)
            {
                number *= otherNumber;
            }

            Assert.AreEqual(
                "2729464718722514012004521014038715989889074373729029728697560600890025436472917198095942595005701042619572336777946519085073537524067555340658871256690094734922463077918154687350928774772373064870869669227502636224931618396069309050647168782921769429184921957982589951774742342775840287918772435268850747113098589025141871866317086129385121719519020071883375422760908720965188954274076497554779052734375",
                number.ToString()
            );
        }

        /// <summary>
        /// Verification PHP code using BC math:
        /// <code>
        /// $a = "5";
        /// $a = bcmul($a, "5542342");
        /// echo $a. "<br />";
        /// $a = bcdiv($a, "3");
        /// echo $a. "<br />";
        /// $a = bcadd($a, bcadd($a, $a));
        /// echo $a. "<br />";
        /// $a = bcmul($a, "2729464718722514012004521014038715989889074373729029728697560600890025436472917198095942595005701042619572336777946519085073537524067555340658871256690094734922463077918154687350928774772373064870869669227502636224931618396069309050647168782921769429184921957982589951774742342775840287918772435268850747113098589025141871866317086129385121719519020071883375422760908720965188954274076497554779052734375");
        /// echo $a. "<br />";
        /// $a = bcdiv($a, "27711708");
        /// echo $a. "<br />";
        /// for ($i = 0; $i != 42; $i++) {
        ///     $a = bcadd($a, "-1");
        ///     $a = bcdiv($a, "3");
        ///     $a = bcmul($a, "2");
        ///     $a = bcsub($a, "1234234");
        /// }
        /// echo $a. "<br />";
        /// $a = bcdiv($a, "534454633665489099786965452412344345365353453345345353453455");
        /// echo $a. "<br />";
        /// for ($i = 0; $i != 4; $i++) {
        ///     $a = bcmul($a, $a);
        /// }
        /// echo $a. "<br />";
        /// </code>
        /// </summary>
        [TestMethod]
        public void TestLongRoutineOfManipulations()
        {
            BigInteger number = new BigInteger("5");

            number *= 5542342;
            Assert.AreEqual("27711710", number.ToString());

            number /= 3;
            Assert.AreEqual("9237236", number.ToString());

            number += number + number;
            Assert.AreEqual("27711708", number.ToString());

            number *= new BigInteger("2729464718722514012004521014038715989889074373729029728697560600890025436472917198095942595005701042619572336777946519085073537524067555340658871256690094734922463077918154687350928774772373064870869669227502636224931618396069309050647168782921769429184921957982589951774742342775840287918772435268850747113098589025141871866317086129385121719519020071883375422760908720965188954274076497554779052734375");
            Assert.AreEqual(
                "75638129281540441326577781020904798206736981435061742964986019684168925008110031301812917177560245628369143681668114776501985030394003065874179167874988951786508699456049150594700231735289768840766597979689138624295527328959301040173291552339043461264899235302401801827315741578239995513440949444619293399580031074276736211732794126228370712635768984678171109769926856290040794465668559869900751113891601562500",
                number.ToString()
            );

            number /= 27711708;
            Assert.AreEqual(
                "2729464718722514012004521014038715989889074373729029728697560600890025436472917198095942595005701042619572336777946519085073537524067555340658871256690094734922463077918154687350928774772373064870869669227502636224931618396069309050647168782921769429184921957982589951774742342775840287918772435268850747113098589025141871866317086129385121719519020071883375422760908720965188954274076497554779052734375",
                number.ToString()
            );

            for (int i = 0; i < 42; i++)
            {
                number += -1;
                number /= 3;
                number *= 2;
                number -= 1234234;
            }

            Assert.AreEqual(
                "109709593176106260970322943036739439169282206410983078123378683509474750508268360354818263128630930879800709639698817401451490117486026231494894338063747289156008957601602013097032007467252698084409504808868444955329491920979756344067642149030495606941038563758083278756142190643340463930405444106054626096973343178298479602459681754579989029720884199397815005621691561437725414825199884548897016",
                number.ToString()
            );

            number /= new BigInteger("534454633665489099786965452412344345365353453345345353453455");

            Assert.AreEqual(
               "205273911508030112615928573096920172352379734787112267831495292190615389941386387332292285834949003559431127312085825877442385593271424184866809515663408736972751890512541213401582425933541310958887081337375616076549808136280618178761585975380683053462202180161936024278175896157587680255634057885736194590657048541506466064896824564004",
               number.ToString()
            );

            for (int i = 0; i < 4; i++)
            {
                number *= number;
            }

            Assert.AreEqual(
               "9938941318949083181747105246535002158906449144758262375148577690849415981980653093301955155859964656540480990402662888485881841893846694012386673950809381643662210247760073215251737269107280516374939208634107742904549777510475217040705349486107540490506669659473209777859698294900221714911985162863489040346538297580660087200847567116179989710950028602762227543742347915687445497223522192995520656089055036498285516645569787190206644662328840152853864184174416117781322163190845435333713119216189828091922334257294041286562573518794200598105422174329973788820962721728247962316795119759944936938578950600144692933235817714864457838377675370989597585007755339876020612975669773069578715936049603663237628664966052469324273598442207218784884936874557203432704433680941686217851418175727356169862543391134639913783122428293970089603378862729733493648357097339729668772263396234686377077212384830779445610820467762294010851494269372060662460320423648781858641071482837870933344222107426194654042612454532243566163427564345582996109293656891731626883028625513660639674343285345363080575994558960798152005341873072280265233471217583334510256143424829758768684795218659849654620614705043911135072423023139544555848883381112498705322407791939009160880294114223331703826988628960773173830522441821539801874379274943140048791956334376011463533843956348320022675557620893262527760363844022316875365721097284523510549607619008812096895375731619322380362520501000954223086830848935076170363016219061134088065888182456812582404665966206681126180516548354610341397356916492118633074817291399745866048274121575191189984386643752710905406501907328217644197916614331690393471416754784945481605016381664517018117493909997294146939557759150656120287729514387756408656355519053572950865705823358130966868341998909643037868122795670591943525556759003879884312411175670131977805905874116911028445879081782760470381863171644379530556034450934609780614240338063100838889574773589106279695818201596573880412526925469607544114277869496276024842691392022956763361704840184558722982865825665471272024028378064019586487336540111097077948515410244752966354155314149152490790084679602036901524245784146665560631952972942219995762246491593082894658986786396635205047195900720108561498976589123689992031032848870291500735722748273204466285218852644280206235758641310565136253767659199914116803703001156591935639599452200192130536455975761557141227645994258827157315737724183277958566529330207601915406733944783719594633848395585785868645026346468186879166097848466613745868905348543408805471089444342215891106623596155132160898468684548459201724825852403152815464872291111814718980289408703562631030103261669949874582713043837990778808512936584724019702421473169338834715318889585344051272766514973216238858511607168865594585535754668715856055052492329063558097133737442803245998334866029085191404070419973099486123638922061712828742416610869413952533771899221196652501076647547373925377356511548885797178225253265162864270865897750451389020077469140580265628308584600486115750899258910659206154177843412965317057887040935387630872115061246339997054464142242194467303300103057061509523022582009711486015660767263303759914836194694692186425780066484383441695901421328731928057781199171210929785548673988179496070859691618613438423473234822406207827757010857968785900967599117614636470959830607811884735965460760421092971609284235845479241533241752927597105628811742153316490371431414246498213779063152811408718012028981854834417688416517322919665982235994073220793231471156053682366196359832590068955748190239728566939842421407956848853327312048315428857126942482536559179306393956504372780148597831484960439063454237068132406316971081939921332917091924819642619266687110446148612725805611418421584242091904732384608136212434409780394614625860945960563462473923967075362024982610372437708407428326276456090416089638017458783366805877279739148356381756096862195432793108047914348453560560081638894908168875550400048782348812009700560194908275131305987661439234622174312558078961573629279686125106710138128357744505140238191364936397294086732152067748520598090680222393391521601778605357737585909250545282754522966531555612139064441154873372929869887343192094969342661854202727038475733976306747327363492322431400167611782296318219918210068112965881901748353259344847547057306615614388937259293153691632134333733332002931689721472231644512188939637235921163148432759698983769762169982614168615545875786715507172296325117481456057068047468807393604060086015320588397404031378049168031852918020470384021072329843511987571842875555499731086158073513290822089792016130879993652783791788293350966644428504282983520803145340673064663163849331478188069867158822058100549888905160512536930146871380743093395086714405449443893289310987415113187276946998795518674480022675777139010110116208212095913465522281565554644210714168035584880588860357124063939601360616695440765563170729481821958706224858673316181627885033329624841040029000741554367990239593379034565336294102411411594638493913969451528919601354149671202163686445617734179686800259771898821098540126345788785396318029417311669162299465552517431899115583214715530788737272025925521787752310601890465319775885479426191228858166048441013832352900175383592542143313115867858610338368719846972214259506017450888762788709820331690948941266060864735250743296",
               number.ToString()
            );
        }

        [TestMethod]
        public void TestGetHashCodeReturnsSameHashCodeForSameNumber()
        {
            foreach (string number in DpNumbers())
            {
                Assert.AreEqual(new BigInteger(number), new BigInteger(number));
            }
        }

        public string[] DpNumbers()
        {
            return new string[]
            {
                "0", "1", "2", "3", "4", "5",
                "-1", "-10", "10", "100", "1000",
                "123904812304987132409","340973214309871234",
                "-123412341234", "-974762927176489", "-0"
            };
        }

        [TestMethod]
        public void TestGetHashCodeReturnsDifferentHashCodeForDifferentNumbers()
        {
            foreach (string[] numbers in DpNotEqualNumbers())
            {
                Assert.AreNotEqual(new BigInteger(numbers[0]), new BigInteger(numbers[1]));
            }
        }

        public string[][] DpNotEqualNumbers()
        {
            return new string[][]
            {
                new string[] { "0", "1" },
                new string[] { "-1", "1" },
                new string[] { "-0", "1" },
                new string[] { "100", "0" },
                new string[] { "5", "-100" },
                new string[] { "1234123414", "555555" },
                new string[] { "1234123414", "1234123415" },
                new string[] { "1234123414", "1234123416" },
                new string[] { "1234123414", "1234123417" },
                new string[] { "1234123414", "1234437573" },
                new string[] {
                    "1234123414123412341412341234141234123414123412341412341234141234123414",
                    "12341234141234123414123412341412341234141234123414123412341412341234141234437573"
                },
            };
        }
    }
}

Tests of my own implementation:

namespace UnitTestProject.BigIntegerTest
{
    [TestClass]
    public class ScientificFormatterTest
    {
        [TestMethod]
        public void TestFormatReturnsScientificNotation()
        {
            BigIntegerFormatter scientificFormatter = new ScientificFormatter();

            foreach (string[] data in DpNumbersAndScientificNotation())
            {
                BigInteger number = new BigInteger(data[0]);
                string expectedScientificNotation = data[1];

                Assert.AreEqual(expectedScientificNotation, scientificFormatter.Format(number));
            }
        }

        public string[][] DpNumbersAndScientificNotation()
        {
            return new string[][]
            {
                new string[] { "1", "1" },
                new string[] { "15", "15" },
                new string[] { "123", "123" },
                new string[] { "999", "999" },
                new string[] { "1000", "1.000e3" },
                new string[] { "1345", "1.345e3" },
                new string[] { "27488", "2.748e4" }, // It shouldn't do rounding
                new string[] { "999999", "9.999e5" },
                new string[] { "123456789", "1.234e8" },
                new string[] { "89403287295908294590218974", "8.940e25" },
                new string[] { "894032872098734590873245098723450978234509782340957823049875234595908294590218974", "8.940e80" },
                new string[] { "8940328720987345902450987234509782345097823409578230498752345959082945902 1897448372615958423412341324", "8.940e100" },
                new string[] {
                    "894032872098734590245098723450978234509782340957823049875234595908294590218974483726159584234123413248940328720987345902450987234509782345097823409578230498752345959082945902189744837261595842341234132489403287209873459024509872345097823450978234095782304987523459590829459021897448372615958423412341324894032872098734590245098723450978234509782340957823049875234595908294590218974483726159584234123413248940328720987345902450987234509782345097823409578230498752345959082945902189744837261595842341234132489403287209873459024509872345097823450978234095782304987523459590829459021897448372615958423412341324894032872098734590245098723450978234509782340957823049875234595908294590218974483726159584234123413248940328720987345902450987234509782345097823409578230498752345959082945902189744837261595842341234132489403287209873459024509872345097823450978234095782304987523459590829459021897448372615958423412341324894032872098734590245098723450978234509782340957823049875234595908294590218974483726159584234",
                    "8.940e1001"
                },
            };
        }
    }
}

Note: I have shortened this test so that it fits into the 65536 character limit.

namespace UnitTestProject.BigIntegerTest
{
    [TestClass]
    public class NumericalformatterTest
    {
        [TestMethod]
        public void TestFormatReturnsNumericalFormat()
        {
            BigIntegerFormatter numericalFormatter = new NumericalFormatter();

            foreach (string[] data in DpNumbersAndNumericalFormat())
            {
                BigInteger number = new BigInteger(data[0]);
                string expectedNumericalFormat = data[1];

                Assert.AreEqual(expectedNumericalFormat, numericalFormatter.Format(number));
            }
        }

        private string[][] DpNumbersAndNumericalFormat()
        {
            return new string[][]
            {
                new string[] { "0", "0" },
                new string[] { "1", "1" },
                new string[] { "15", "15" },
                new string[] { "123", "123" },
                new string[] { "999", "999" },
                new string[] { "1000", "1000" },
                new string[] { "9999", "9999" },
                new string[] { "10000", "10.000K" },
                new string[] { "78456", "78.456K" },
                new string[] { "134777", "134.777K" },
                new string[] { "999999", "999.999K" },
                new string[] { "1000000", "1.000M" },
                new string[] { "12345000", "12.345M" },
                new string[] { "999999000", "999.999M" },
                new string[] { "1000000000", "1.000B" },
                new string[] { "12345678900", "12.345B" },
                new string[] { "123345678900", "123.345B" },
                new string[] { "1233000000000", "1.233T" },
                new string[] { "9999000000000", "9.999T" },
                new string[] { "12233000000000", "12.233T" },
                new string[] { "99999000000000", "99.999T" },
                new string[] { "100000000000000", "100.000T" },
                new string[] { "456789000000000", "456.789T" },
                new string[] { "999999000000000", "999.999T" },
                new string[] { "1000000000000000", "1.000Q" },
                new string[] { "10000000000000000", "10.000Q" },
                new string[] { "100000000000000000", "100.000Q" },
                new string[] { "999999000000000000", "999.999Q" },
                new string[] { "1000000000000000000", "1.000a" },
                new string[] { "10000000000000000000", "10.000a" },
                new string[] { "100000000000000000000", "100.000a" },
                new string[] { "1000000000000000000000", "1.000b" },
                new string[] { "1000000000000000000000000", "1.000c" },
                new string[] { "1000000000000000000000000000", "1.000d" },
                new string[] { "1000000000000000000000000000000", "1.000e" },
                new string[] { "1000000000000000000000000000000000", "1.000f" },
                new string[] { "1000000000000000000000000000000000000", "1.000g" },
                new string[] { "1000000000000000000000000000000000000000", "1.000h" },
                new string[] { "1000000000000000000000000000000000000000000", "1.000i" },
                new string[] { "1000000000000000000000000000000000000000000000", "1.000j" },
                new string[] { "1000000000000000000000000000000000000000000000000", "1.000k" },
                new string[] { "1000000000000000000000000000000000000000000000000000", "1.000l" },
                new string[] { "1000000000000000000000000000000000000000000000000000000", "1.000m" },
                new string[] { "1000000000000000000000000000000000000000000000000000000000", "1.000n" },
                new string[] { "1000000000000000000000000000000000000000000000000000000000000", "1.000o" },
                new string[] { "1000000000000000000000000000000000000000000000000000000000000000", "1.000p" },
                new string[] { "1000000000000000000000000000000000000000000000000000000000000000000", "1.000q" },
                new string[] { "1000000000000000000000000000000000000000000000000000000000000000000000", "1.000r" },
                new string[] { "1000000000000000000000000000000000000000000000000000000000000000000000000", "1.000s" },
                new string[] { "1000000000000000000000000000000000000000000000000000000000000000000000000000", "1.000t" },
                new string[] { "1000000000000000000000000000000000000000000000000000000000000000000000000000000", "1.000u" },
                new string[] { "1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "1.000x" },
                new string[] { "1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "1.000y" },
                new string[] { "1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "1.000z" },
                new string[] { "1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "1.000aa" },
                new string[] { "1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "1.000ab" },
                new string[] { "1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "1.000au" },
                new string[] { "1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "1.000av" },
                new string[] { "1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "1.000aw" },
                new string[] { "1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "1.000ax" },
                new string[] { "1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "1.000ay" },
                new string[] { "1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "1.000az" },
                new string[] { "1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "1.000ba" },
                new string[] { "1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "1.000cc" },
                new string[] { "1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "1.000cd" },
                new string[] { "1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "1.000ce" },
                new string[] { "1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "1.000ct" },
                new string[] { "1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "1.000cy" },
                new string[] { "1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "1.000cz" },
                new string[] { "1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "1.000da" },
                new string[] { "1234000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "1.234da" },
                new string[] { "123456000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "123.456da" },
                new string[] { "1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "1.000sa" },
                new string[] { "1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "1.000sy" },
                new string[] { "1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "1.000zz" },
                new string[] { "10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "10.000zz" },
                new string[] { "100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "100.000zz" },
                new string[] { "1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "1.000aaa" },
                new string[] { "1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "1.000aba" },
                new string[] { "1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "1.000aca" },
            };
        }
    }
}

For testing the CachedFormatter I created a NumericalFormatterMock that extends the NumericalFormatter. I did this because I was unable to find a mocking framework for Unity/Visual Studio/C#

namespace UnitTestProject.BigIntegerTest
{
    [TestClass]
    public class CachedFormatterTest
    {
        private const int DEFAULT_CACHE_SIZE = 100;
        NumericalFormatterMock numericalFormatterMock;

        // Called before every test to ensure that a fresh Mock is used
        [TestInitialize]
        public void TestInitialize()
        {
            numericalFormatterMock = new NumericalFormatterMock();
        }

        [TestMethod]
        public void TestFormatUsesCache()
        {
            BigIntegerFormatter cachedFormatter = new CachedFormatter(numericalFormatterMock, DEFAULT_CACHE_SIZE);

            Assert.AreEqual("1.000M", cachedFormatter.Format(new BigInteger("1000000")));
            Assert.AreEqual(1, numericalFormatterMock.amountOfTimesFormatIsCalled);

            Assert.AreEqual("1.000M", cachedFormatter.Format(new BigInteger("1000000")));
            Assert.AreEqual(1, numericalFormatterMock.amountOfTimesFormatIsCalled);

            Assert.AreEqual("1.000M", cachedFormatter.Format(new BigInteger("1000001")));
            Assert.AreEqual(2, numericalFormatterMock.amountOfTimesFormatIsCalled);

            Assert.AreEqual("1.000M", cachedFormatter.Format(new BigInteger("1000001")));
            Assert.AreEqual(2, numericalFormatterMock.amountOfTimesFormatIsCalled);
        }

        [TestMethod]
        public void TestFormatUtilizesCacheUpToCacheSize()
        {
            BigIntegerFormatter cachedFormatter = new CachedFormatter(numericalFormatterMock, DEFAULT_CACHE_SIZE);

            for (int i = 0; i < DEFAULT_CACHE_SIZE; i++)
            {
                Assert.AreEqual(i.ToString(), cachedFormatter.Format(new BigInteger(i)));
                Assert.AreEqual(i + 1, numericalFormatterMock.amountOfTimesFormatIsCalled);
            }

            Assert.AreEqual(DEFAULT_CACHE_SIZE, numericalFormatterMock.amountOfTimesFormatIsCalled);

            // All of these calls should be returned from cache
            for (int i = 0; i < DEFAULT_CACHE_SIZE; i++)
            {
                Assert.AreEqual(i.ToString(), cachedFormatter.Format(new BigInteger(i)));
            }
            Assert.AreEqual(DEFAULT_CACHE_SIZE, numericalFormatterMock.amountOfTimesFormatIsCalled);
        }

        [TestMethod]
        public void TestFormatRemovesOldestItemFromCacheWhenFull()
        {
            int maxCacheSize = 3;
            BigIntegerFormatter cachedFormatter = new CachedFormatter(numericalFormatterMock, maxCacheSize);

            Assert.AreEqual("0", cachedFormatter.Format(new BigInteger("0")));
            Assert.AreEqual("1", cachedFormatter.Format(new BigInteger("1")));
            Assert.AreEqual("2", cachedFormatter.Format(new BigInteger("2")));
            Assert.AreEqual(3, numericalFormatterMock.amountOfTimesFormatIsCalled);

            Assert.AreEqual("3", cachedFormatter.Format(new BigInteger("3")));
            Assert.AreEqual(4, numericalFormatterMock.amountOfTimesFormatIsCalled);

            // Oldest should now be gone from cache
            Assert.AreEqual("0", cachedFormatter.Format(new BigInteger("0")));
            Assert.AreEqual(5, numericalFormatterMock.amountOfTimesFormatIsCalled);

            // By adding "0" to the cache "1" should be gone
            Assert.AreEqual("1", cachedFormatter.Format(new BigInteger("1")));
            Assert.AreEqual(6, numericalFormatterMock.amountOfTimesFormatIsCalled);
        }


        [TestMethod]
        public void TestCachedFormatterIsQuicker()
        {
            CachedFormatter cachedFormatter = new CachedFormatter(new ScientificFormatter(), 100);
            ScientificFormatter scientificFormatter = new ScientificFormatter();

            Stopwatch cachedFormatterStopwatch = Stopwatch.StartNew();
            Format100BigIntegers(cachedFormatter);
            Format100BigIntegers(cachedFormatter);
            cachedFormatterStopwatch.Stop();

            Stopwatch ScientificFormatterStopwatch = Stopwatch.StartNew();
            Format100BigIntegers(scientificFormatter);
            Format100BigIntegers(scientificFormatter);
            ScientificFormatterStopwatch.Stop();

            Debug.WriteLine(ScientificFormatterStopwatch.Elapsed);
            Debug.WriteLine(cachedFormatterStopwatch.Elapsed);

            Assert.IsTrue(cachedFormatterStopwatch.Elapsed < ScientificFormatterStopwatch.Elapsed);
        }

        public void Format100BigIntegers(BigIntegerFormatter bigIntegerformatter)
        {
            for (int i = 0; i < 100; i++)
            {
                int singleDigit = i % 10 + 1;
                bigIntegerformatter.Format(new BigInteger(new String(singleDigit.ToString()[0], 333)));
            }
        }

        [TestMethod]
        public void TestCachedFormatterIsQuickerWithSingleNumber()
        {
            CachedFormatter cachedFormatter = new CachedFormatter(new ScientificFormatter(), 100);
            ScientificFormatter scientificFormatter = new ScientificFormatter();

            BigInteger number = new BigInteger(new String('1', 2000));

            Stopwatch cachedFormatterStopwatch = Stopwatch.StartNew();
            FormatNumberXTimes(cachedFormatter, number, 100);
            cachedFormatterStopwatch.Stop();

            Stopwatch ScientificFormatterStopwatch = Stopwatch.StartNew();
            FormatNumberXTimes(scientificFormatter, number, 100);
            ScientificFormatterStopwatch.Stop();

            Debug.WriteLine(ScientificFormatterStopwatch.Elapsed);
            Debug.WriteLine(cachedFormatterStopwatch.Elapsed);

            Assert.IsTrue(cachedFormatterStopwatch.Elapsed < ScientificFormatterStopwatch.Elapsed);
        }

        private void FormatNumberXTimes(BigIntegerFormatter formatter, BigInteger number, int count)
        {
            for (int i = 0; i < count; i++)
            {
                formatter.Format(number);
            }
        }

        [TestMethod]
        public void TestMockWorksOk()
        {
            Assert.AreEqual("654.346T", numericalFormatterMock.Format(new BigInteger("654346456363546")));
            Assert.AreEqual(1, numericalFormatterMock.amountOfTimesFormatIsCalled);

            Assert.AreEqual("655.346T", numericalFormatterMock.Format(new BigInteger("655346456363546")));
            Assert.AreEqual(2, numericalFormatterMock.amountOfTimesFormatIsCalled);
        }
    }

    internal class NumericalFormatterMock : NumericalFormatter, BigIntegerFormatter
    {
        public int amountOfTimesFormatIsCalled = 0;

        public string Format(BigInteger number)
        {
            amountOfTimesFormatIsCalled++;
            return base.Format(number);
        }
    }
}
\$\endgroup\$
  • 3
    \$\begingroup\$ Not enough meat for a review, so i'll just post them here: No need for shouting: PRECOMPUTE_FOUR_CHARACTERS. Also, method names should be Pascalcase: private static List<string> preComputeBase27Values(). \$\endgroup\$ – BCdotWEB Jun 21 '16 at 10:47
  • 1
    \$\begingroup\$ @BCdotWEB I think your example is camelCase rather than PascalCase. \$\endgroup\$ – RJFalconer Jun 21 '16 at 13:30
  • 1
    \$\begingroup\$ @RJFalconer I quoted the "incorrect" code, didn't give corrected versions. \$\endgroup\$ – BCdotWEB Jun 21 '16 at 13:31
  • 3
    \$\begingroup\$ @corsiKa doubles are notoriously bad when accumulating over long timespans. Taking your comment as an example, adding 1 to 100000000000000000000000000000000 would still return the same value, causing for example score accumulation to break down. \$\endgroup\$ – pipe Jun 22 '16 at 1:44
  • 1
    \$\begingroup\$ @ThijsRiezebeek A double is too LOW for you? You're going to go up to 2^1000 power? You need to lower your scaler values. There are other ways to have infinite progression and still have low scalers. Off the top of my head, you can have healing towers which will give you another variable. But even at 2^1000 even if you double your values every level (which, let's be honest, what's the point that isn't progression) that's still over 900 levels. If players do 900 levels, worry about fixing it then, not now. \$\endgroup\$ – corsiKa Jun 22 '16 at 15:22
13
\$\begingroup\$

Naming

Interfaces are usually named starting with a capital I in .NET.

ScientificFormatter

Here you have public string Format(BigInteger number) and private string FormatNumberString(string numberString) where the second would be easier to read if you would use method overloading. Method overloading is just that you can have a method with the same name which differs in the methods signature. Just keep in mind that the signature doesn't include the return type of a method.

That being said the second method could look like so

private string Format(string numberString)
{
    if (numberString.Length < 4)
    {
        return numberString;
    }

    int exponent = numberString.Length - 1;
    string leadingDigit = numberString.Substring(0, 1);
    string decimals = numberString.Substring(1, 3);

    return String.Format("{0}.{1}e{2}", leadingDigit, decimals, exponent);
}

CachedFormatter

Using exceptions to control your program flow isn't that good. If you have the possibility to avoid exceptions you should always do this.

public string Format(BigInteger number)
{
    try
    {
        return FromCache(number);
    }
    catch (KeyNotFoundException)
    {
        string formattedNumber = bigIntegerFormatter.Format(number);
        AddToCache(number, formattedNumber);

        return formattedNumber;
    }
}  

here FromCache() is trying to get a value from a dictionary by using a key and if this fails an exception is thrown and catched to insert the to be formatted number into the dictionary.

Edit based on your comment

However, I disagree with your opinion on exceptions. I think that functions should do one thing and if they fail they should throw an exception. This also improves readability in my opinion as I think that the CachedFormatter.Format() is much more readable when using the exception than when introducing if statements.

Sure a method should do only one thing, but having a method just for the sake of having one is just overengeniering. What advantage do you gain by having

private string FromCache(BigInteger number)
{
    return numberCache[number];
}  

absolutely none IMO. You are adding a method to read a property which is harder to read because you need to jump around in your code.

If that would be some different, more complex method I would agree, but replacing a simple if statement with a try..catch to simply wrap a method call which only reads a property is just too much.

See also: Are exceptions as control-flow considered a serious antipattern if so why

The Dictionary<TKey, TValue> provides the handy TryGetValue() method which avoids the usage of exceptions to control the flow.

Using TryGetValue() would lead to

public string Format(BigInteger number)
{
    string formattedNumber = null;
    if (numberCache.TryGetValue(number, out formattedNumber))
    {
        return formattedNumber;
    }

    string formattedNumber = bigIntegerFormatter.Format(number);
    AddToCache(number, formattedNumber);

    return formattedNumber;
}  

which omits the need to have FromCache() as well.


In the AddToCache() method you are using the IEnumerable<T>.Count() method but the Count property of the Dictionary would be sufficient like so

private void AddToCache(BigInteger number, string formattedNumber)
{
    listOfCachedNumbers.AddLast(number);
    numberCache.Add(number, formattedNumber);

    if (this.numberCache.Count > this.maxCacheSize)
    {
        RemoveOldestNumberFromCache();
    }
}

You ain't doing any argument validation in the constructor of the CachedFormatter which can lead to some strange behaviour and ArgumentNullException's.

If the passed in BigIntegerFormatter is null any call to Format() will result in an ArgumentNullException exposing implementation details of your class via the stacktrace.

A passed in maxCacheSize <= 0 will lead to a significant loss in performance because each time the AddToCache() method is called the added number and formattedNumber are removed immediately hence no caching at all.

In addition you should initielize the Dictionary inside the constructor to make use of the maxCacheSize argument to set the initial capacity of the Dictionary which removes the need for the Dictionary to dynamically increase its size after reaching the initial capazity which is by default 0 and increased after reaching that value to the next prime number.

So better do

numberCache = new Dictionary<BigInteger, string>(maxCacheSize);  

after you have checked the validness of maxCacheSize.

\$\endgroup\$
  • \$\begingroup\$ Thank you for the feedback! I completely forgot about overloading. However, I disagree with your opinion on exceptions. I think that functions should do one thing and if they fail they should throw an exception. This also improves readability in my opinion as I think that the CachedFormatter.Format() is much more readable when using the exception than when introducing if statements. \$\endgroup\$ – Thijs Riezebeek Jun 21 '16 at 10:48
  • 8
    \$\begingroup\$ Please, please, please don't use exceptions for control flow. From Microsoft: "Throwing or handling an exception consumes a significant amount of system resources and execution time. Throw exceptions only to handle truly extraordinary conditions, not to handle predictable events or flow control." - source MSDN \$\endgroup\$ – theosza Jun 21 '16 at 11:55
  • 3
    \$\begingroup\$ I seem to remember years ago, reading that just having try/catch would add about 10,000 instruction cycles to your code. If the exception was actually thrown, that number is upwards of 1M. JIT languages (C#/Java etc) would be even more. it's not worth it, if you can just use TryGetValue and not pay any penalty. Exceptions are for exceptional circumstances. if you never expect FromCache to fail, then your method is fine, but I suspect FromCache will fail fairly frequently (especially on first run). \$\endgroup\$ – Neil Jun 21 '16 at 14:59
  • 1
    \$\begingroup\$ @Pharap: That is perhaps the most generous way of describing my killing a little time while waiting for the tests to finish that I've heard yet. :) \$\endgroup\$ – Eric Lippert Jun 21 '16 at 19:39
  • 2
    \$\begingroup\$ But yes, you should reserve exceptions for exceptional circumstances. A cache miss is not exceptional, a cache miss is normal, so don't throw an exception on a cache miss. Moreover, remember that the point of the cache is to improve performance. If you haven't got a measurement that shows how the cache improves performance, then you don't know whether it is doing its job or not. What do your measurements tell you about using exceptions on the cache miss path? \$\endgroup\$ – Eric Lippert Jun 21 '16 at 19:41
6
\$\begingroup\$

This isn't a full review, it's just a couple of initial thoughts:

Unlike Java, Interfaces in C# are usually prefixed with 'I', so BigIntegerFormatter becomes IBigIntegerFormatter.

If you aren't committed to using the microsoft test framework, you might want to consider using something like Nunit, which supports the TestCase attribute. If you use the NUnit Test Adapter, you can still run the tests from within visual studio, but it would help you to remove some of your loops, for example:

[TestFixture]
public class BigIntegerTest
{
    [TestCase("0", "0", "0")]
    [TestCase("1", "0", "1")]
    [TestCase("-1", "0", "-1")]
    public void TestAddition(string left, string right, string expectedOutcome)
    {
        BigInteger leftOperand = new BigInteger(left);
        BigInteger rightOperand = new BigInteger(right);

        Assert.AreEqual(expectedOutcome, (leftOperand + rightOperand).ToString());
    }
\$\endgroup\$
  • \$\begingroup\$ Personally I dislike prefixing interfaces with an 'I', but since C# doesn't have the implements keyword and its pretty much a standard, I'll do it. Also thanks for the Nunit pointer, I will try it out and see if I like it. \$\endgroup\$ – Thijs Riezebeek Jun 21 '16 at 10:50
  • \$\begingroup\$ @ThijsRiezebeek What does the lack of the implements keyword have to do with anything? \$\endgroup\$ – Pharap Jun 21 '16 at 17:49
  • \$\begingroup\$ Java differentiates between implements and extends, whilst C# only has the : keyword. This means that a class declaration like A : B, C, would be ambiguous since you can't tell which is an interface and which an abstract class. However, this probably matters very little due to conventions of putting any extending class first and coloring in IDEs. \$\endgroup\$ – Thijs Riezebeek Jun 22 '16 at 18:43
2
\$\begingroup\$

This:

private static List<string> preComputeBase27Values()
{
    List<string> preComputedValues = new List<string>();

    for (int i = 0; i < PRECOMPUTE_FOUR_CHARACTERS; i++)
    {
        string text = ToBase27AlphaString(i);

        if (!text.Contains('`'))
        {
            preComputedValues.Add(text);
        }
    }

    return preComputedValues;
}

can be written using LINQ for more readability and conciseness:

private static List<string> preComputeBase27Values() =>
    Enumerable
        .Range(0, PRECOMPUTE_FOUR_CHARACTERS)
        .Select(ToBase27AlphaString)
        .Where(x => !x.Contains('`'))
        .ToList();
\$\endgroup\$
0
\$\begingroup\$

Using this. to access member variables is not required - in fact, many C# coding standards mandate naming schemes (such as member variables must start with an underscore) to avoid having to use this. The main exception to this is when you don't want to use the prefix, you will generally need to use this.member_name to differentiate between the parameter and the member.

Personally, I used to be pretty strongly against using the _ (I believe it stems from compatibility with VB) but it is so common in C# projects that I've succumbed.

\$\endgroup\$
  • \$\begingroup\$ I disagree about dropping the this, I've seen naming schemes enforce using this because it's an unambiguous lanugage keyword whose primary benefits include not having to have a special naming scheme for fields. I agree with use of underscores being uncommon in C#, except in one of the aforementioned naming schemes in which people prefix private fields with m_ where m stands for 'member', e.g. m_numberCache. \$\endgroup\$ – Pharap Jun 21 '16 at 18:24
  • \$\begingroup\$ So the primary benefit is typing 5 extra characters instead of one underscore? That's not a benefit in my book. Also, even VS now suggests that 'this.' is not required (via intellisense suggestions). \$\endgroup\$ – Neil Jun 22 '16 at 7:52
  • \$\begingroup\$ The benefit is unambiguity. By seeing this.something one instantly knows that something is a member of the class just by knowing the language (i.e. not having to know what naming convention is in use). It's not about avoiding underscores. I mention m_ because people try to use it as a substitute for this.. As far as I'm concerned implicit clarity is more important than explicit brevity. \$\endgroup\$ – Pharap Jun 22 '16 at 17:22

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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