3

I am trying to create an endless game such as Tap Titans, Clicker Heroes, etc. I have a BigInteger class that is capable of representing arbitrarily large integers as long as they fit into memory.

Now I have a class that formats a BigInteger into a specific format. It uses K (thousand), M (million), B (billion), T (trillion), Q (quadrillion) for the 'smaller' numbers, but after that the short hand notations become ambiguous and unintuitive. Q is already ambiguous due to Quintillion, but I can live with that.

After Q, I want to start from the letter a. So 1000Q = 1.000a, then 1000a = 1.000b etc. When 1000z is reached, it should be formatted to 1.000aa. Then 1000aa = 1.000 ab, 1000 az = 1.000 ba, 1000 bz = 1.000 ca, etc.

So far I have achieved the above, however my class is unable to format a number after 1000zz. I have not been able to come up with a generic algorithm that automatically determines how many characters are needed (could be aaaz for extremely large numbers).

My class looks as follows:

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

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

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

        return FormatGeneral(number);
    }

    private 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 string CreateNumericalFormat(string leadingNumbers, string decimals, string suffix)
    {
        return String.Format("{0}.{1}{2}", leadingNumbers, decimals, suffix);
    }

    private 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 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 string GetProceduralSuffix(int numberOfThousandsAfterQ)
    {
        if (numberOfThousandsAfterQ < 27)
        {
            return ((char)(numberOfThousandsAfterQ + 96)).ToString();
        }

        int rightChar = (numberOfThousandsAfterQ % 26);
        string right = rightChar == 0 ? "z" : ((char)(rightChar + 96)).ToString();
        string left = ((char)(((numberOfThousandsAfterQ - 1) / 26) + 96)).ToString();

        return left + right;
    }
}

As you can see the getProceduralSuffix() method cannot handle BigIntegers that would result in more than two character suffixes.

I also have a unit test that verifies the functionality of this class (prepare for some side scrolling):

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[] { "1000000000000000000000000000000000000000000000000000000000000000000000000000000000", "1.000v" },
                new string[] { "1000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "1.000w" },
                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[] { "1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "1.000ac" },
                new string[] { "1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "1.000ad" },
                new string[] { "1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "1.000ae" },
                new string[] { "1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "1.000af" },
                new string[] { "1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "1.000ag" },
                new string[] { "1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "1.000ah" },
                new string[] { "1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "1.000ai" },
                new string[] { "1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "1.000aj" },
                new string[] { "1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "1.000ak" },
                new string[] { "1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "1.000al" },
                new string[] { "1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "1.000am" },
                new string[] { "1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "1.000an" },
                new string[] { "1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "1.000ao" },
                new string[] { "1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "1.000ap" },
                new string[] { "1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "1.000aq" },
                new string[] { "1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "1.000ar" },
                new string[] { "1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "1.000as" },
                new string[] { "1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "1.000at" },
                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[] { "1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "1.000bb" },
                new string[] { "1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "1.000bc" },
                new string[] { "1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "1.000bd" },
                new string[] { "1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "1.000be" },
                new string[] { "1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "1.000bf" },
                new string[] { "1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "1.000bg" },
                new string[] { "1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "1.000bh" },
                new string[] { "1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "1.000bi" },
                new string[] { "1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "1.000bj" },
                new string[] { "1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "1.000bt" },
                new string[] { "1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "1.000by" },
                new string[] { "1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "1.000bz" },
                new string[] { "1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "1.000ca" },
                new string[] { "1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "1.000cb" },
                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[] { "1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "1.000db" },
                new string[] { "1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "1.000dr" },
                new string[] { "1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "1.000dz" },
            };
        }
    }
}

All the tests above pass at the moment. The tests that are missing is one that checks if 1000 ^ (26 ^ 3 + 5) (the +5 is because the a formatting starts after Q) can be formatted into "1.000aaa".

How do I go about formatting procedurally large integers in the way I have described above.

Thijs Riezebeek
  • 1,762
  • 1
  • 15
  • 22

2 Answers2

1

In the world of numerical notation, this is actually a solved problem. That is, you could instead use scientific notation to represent these especially large numbers. Scientific notation is compact, allows arbitrary precision for the mantissa, and readily understandable. Personally, that's the approach I'd take.

For the sake of discussion, let's look at what other alternatives you have…


On the face of it, your request boils down to a straight numeric base value to text conversion. Just as we can convert a numeric value to its textual representation in, for example, base 2, base 10, base 16, etc. we can convert a numeric value to a textual representation using base 26, using just the letters a through z as the digits.

Then your GetProceduralSuffix() method would look something like this:

static string GetProceduralSuffix(int value)
{
    StringBuilder sb = new StringBuilder();

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

        sb.Append((char)('a'+ digit));
        value /= 26;
    }

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

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

where the Reverse() extension method is this:

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;
    }
}

However, there's a slight problem with the above. In base 26 represented this way, the digit a corresponds to 0, and so your suffixes will never start with the letter a, at least not after the first one (that's a special case, just like when using decimal notation we use the digit 0 by itself to represent the value of zero). Instead, for example, you'll get ba after z and baa after zz.

Personally, I think that's fine. It would preclude suffixes like aaaz, but only because the suffix notation system would be logical, predictable, and easily reversed (i.e. given a suffix, it's trivial to figure out what that means numerically).

However, if you insist on a sequence like az, aazz, aaazzz, aaaa… and so on, you can use base 27 instead of 26, with a character other than az for the 0 digit, and precompute the suffixes skipping values that would have a 0 digit as you go, and then index the result. For example:

List<string> _hackedValues = new List<string>();

static void PrecomputeValues()
{
    // 531441 = 27 ^ 4, i.e. the first 5-digit base 27 number.
    // That's a large enough number to ensure that the output
    // include "aaaz", and indeed almost all of the 4-digit
    // base 27 numbers
    for (int i = 0; i < 531441; i++)
    {
        string text = ToBase27AlphaString(i);

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

static string GetProceduralSuffix(int value)
{
    if (hackedValues.Count == 0)
    {
        PrecomputeValues();
    }

    return _hackedValues[value];
}

static string ToBase27AlphaString(int value)
{
    StringBuilder sb = new StringBuilder();

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

        sb.Append((char)('`'+ digit));
        value /= 27;
    }

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

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

Here is a complete program that illustrates both techniques, showing the text for "interesting" input (i.e. where the number of characters in the output changes):

class Program
{
    static void Main(string[] args)
    {
        int[] values = { 0, 25, 26, 675, 676 };

        foreach (int value in values)
        {
            Console.WriteLine("{0}: {1}", value, ToBase26AlphaString(value));
        }

        Console.WriteLine();

        List<Tuple<int, string>> hackedValues = new List<Tuple<int, string>>();

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

            if (!text.Contains('`'))
            {
                hackedValues.Add(Tuple.Create(i, text));
            }
        }

        Tuple<int, string> prev = null;

        for (int i = 0; i < hackedValues.Count; i++)
        {
            Tuple<int, string> current = hackedValues[i];

            if (prev == null || prev.Item2.Length != current.Item2.Length)
            {
                if (prev != null)
                {
                    DumpHackedValue(prev, i - 1);
                }
                DumpHackedValue(current, i);
            }

            prev = current;
        }
    }

    private static void DumpHackedValue(Tuple<int, string> hackedValue, int i)
    {
        Console.WriteLine("{0}: {1} (actual value: {2})", i, hackedValue.Item2, hackedValue.Item1);
    }

    static string ToBase26AlphaString(int value)
    {
        return ToBaseNAlphaString(value, 'a', 26);
    }

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

    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();
    }
}

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;
        }
    }
}
Peter Duniho
  • 68,759
  • 7
  • 102
  • 136
  • Thank you for this! I have incorporated this into my program and it works :) Could you perhaps elaborate on why you chose 531441? Also, I already have a ScientificFormatter which uses scientific notation, however I will be giving the user the option to switch between the two modes, as this way of displaying it might be less confusing for some and it has a different feel. – Thijs Riezebeek Jun 19 '16 at 22:01
  • 1
    @Thijs: see edit. I've added a comment explaining my choice of the upper bound for that loop. Naturally, you can use any number that suffices for your specific scenario, with the obvious time/memory trade-offs. – Peter Duniho Jun 19 '16 at 22:33
1

After a long time of thinking (a month of avoiding the work actually) and a few hours of coding, I used a part of your code to create my own solution.

It uses prefixes in order: empty string, k, M, B, Q, a, b ... z (excluding k because of thousands), aa, bb, ..., zz, aaa, bbb, ..., zzz, and so on. It trims the zeros from the end of the number, e.g. 1000 = 1k.

(There's also a possibility to use the scientific notation but it doesn't trim zeros.)

using System.Collections.Generic;
using System.Numerics;

/// <summary>
/// Static class used to format the BigIntegers.
/// </summary>
public static class BigIntegerFormatter
{
    private static List<string> suffixes = new List<string>();

    /// <summary>
    /// If it's equal to 0, there are only suffixes from an empty string to Q on the suffixes list.
    /// If it's equal to 1, there are a - z suffixes added.
    /// If it's equal to 2, there are aa - zz suffixes added and so on.
    /// </summary>
    private static int suffixesCounterForGeneration = 0;

    /// <summary>
    /// Formats BigInteger using scientific notation. Returns a number without the exponent if the length
    /// of the number is smaller than 4.
    /// </summary>
    /// <param name="number">Number to format.</param>
    /// <returns>Returns string that contains BigInteger formatted using scientific notation.</returns>
    public static string FormatScientific(BigInteger number)
    {
        return FormatNumberScientificString(number.ToString());
    }

    /// <summary>
    /// Formats BigInteger using engineering notation - with a suffix. Returns a number without the
    /// suffix if the length of the number is smaller than 4.
    /// </summary>
    /// <param name="number">Number to format.</param>
    /// <returns>Returns string that contains BigInteger formatted using engineering notation.</returns>
    public static string FormatWithSuffix(BigInteger number)
    {
        return FormatNumberWithSuffixString(number.ToString());
    }

    private static string FormatNumberScientificString(string numberString)
    {
        // if number length is smaller than 4, just returns the number
        if (numberString.Length < 4) return numberString;

        // Exponent counter. E.g. for 1000 it will be 3 and the number will
        // be presented as 1.000e3 because 1000.Length = 4
        var exponent = numberString.Length - 1;

        // Digit before a comma. Always only one.
        var leadingDigit = numberString.Substring(0, 1);

        // Digits after a comma. Always three of them.
        var decimals = numberString.Substring(1, 3);

        // Returns the number in scientific format. 
        // Example: 12345 -> 1.234e4
        return $"{leadingDigit}.{decimals}e{exponent}";
    }

    private static string FormatNumberWithSuffixString(string numberAsString)
    {
        // if number length is smaller than 4, just returns the number
        if (numberAsString.Length < 4) return numberAsString;

        // Counts scientific exponent. This will be used to determine which suffix from the 
        // suffixes List should be used. 
        var exponentIndex = numberAsString.Length - 1;

        // Digits before a comma. Can be one, two or three of them - that depends on the exponentsIndex.
        var leadingDigit = "";

        // Digits after a comma. Always three of them or less, if the formatted number will have zero 
        // on its end.
        var decimals = "";

        // Example: if the number the methods is formatting is 12345, exponentsIndex is 4, 4 % 3 = 1. 
        // There will be two leading digits. There will be three decimals. Formatted number will look like:
        // 12.345k
        switch (exponentIndex % 3)
        {
            case 0:
                leadingDigit = numberAsString.Substring(0, 1);
                decimals = numberAsString.Substring(1, 3);
                break;

            case 1:
                leadingDigit = numberAsString.Substring(0, 2);
                decimals = numberAsString.Substring(2, 3);
                break;

            case 2:
                leadingDigit = numberAsString.Substring(0, 3);
                decimals = numberAsString.Substring(3, 3);
                break;
        }

        // Trims zeros from the number's end.
        var numberWithoutSuffix = $"{leadingDigit}.{decimals}";
        numberWithoutSuffix = numberWithoutSuffix.TrimEnd('0').TrimEnd('.');

        var suffix = GetSuffixForNumber(exponentIndex / 3);

        // Returns number in engineering format.
        // return $"{numberWithoutSuffix}{suffixes[exponentIndex / 3]}";

        return $"{numberWithoutSuffix}{suffix}";
    }

    /// <summary>
    /// Gets suffix under a given index which is actually a number of thousands.
    /// </summary>
    /// <param name="suffixIndex">Suffix index. Number of thousands.</param>
    /// <returns>Suffix under a given index - suffix for a given number of thousands.</returns>
    private static string GetSuffixForNumber(int suffixIndex)
    {
        // Creates initial suffixes List with an empty string, k, M, B and Q
        if (suffixes.Count == 0) suffixes = CreateSuffixesList();

        // Fills the suffixes list if there's a need to
        if (suffixes.Count - 1 < suffixIndex) FillSuffixesList(suffixes, suffixIndex);

        return suffixes[suffixIndex];
    }

    private static List<string> CreateSuffixesList()
    {
        var suffixesList = new List<string>
        {
            "", "k", "M", "B", "Q"
        };

        return suffixesList;
    }

    private static void FillSuffixesList(List<string> suffixesList, int suffixIndex)
    {
        // while the suffixes list length - 1 is smaller than the suffix index of the suffix that we need
        // (e.g.: when there's a need for an 'a' suffix:
        // when suffixesList = "", "k", "M", "B", "Q"
        // suffixesList.Count = 5, suffixIndex for a 'Q' is 4,
        // suffixIndex for an 'a' is 5)
        while (suffixesList.Count - 1 < suffixIndex)
        {
            // happens only once, when suffixList is filled only with 
            // initial values
            if (suffixesCounterForGeneration == 0)
            {
                for (int i = 97; i <= 122; i++)
                {
                    // k excluded because of thousands suffix
                    if (i == 107) continue;

                    // cache the character a - z
                    char character = (char)i;
                    suffixesList.Add(char.ToString(character));
                }

                suffixesCounterForGeneration++;
            }
            else
            {
                // for every character (a - z) counts how many times the character should be generated as the suffix
                for (var i = 97; i <= 122; i++)
                {
                    // cache the character a - z
                    char character = (char)i;

                    // placeholder for a generated suffix
                    string generatedSuffix = "";

                    // counts how many times one character should be used as one suffix and adds them
                    // basing on the suffixesCounterForGeneration which is the number telling us how many times 
                    // the suffixes were generated
                    for (var counter = 1; counter <= suffixesCounterForGeneration + 1; counter++)
                    {
                        generatedSuffix += character.ToString();
                    }

                    // adds the generated suffix to the suffixes list
                    suffixesList.Add(generatedSuffix);
                }

                suffixesCounterForGeneration++;
            }
        }
    }
}
sjk
  • 111
  • 8