using System; using System.Globalization; using System.Text; using System.Threading; using UnitTestSharp; namespace Vellum.UnitTests { public unsafe class NumberFormatterTests : TestFixture { public static readonly StringBuilder Builder = new StringBuilder(128); public static double MinNormal = BitConverter.Int64BitsToDouble(0x0010000000000000L); public static double MaxSubnormal = BitConverter.Int64BitsToDouble(0x000FFFFFFFFFFFFFL); public static float MinNormalFloat = FloatFromBits(0x00800000); public static float MaxSubnormalFloat = FloatFromBits(0x007FFFFF); public static float FloatFromBits(int bits) { return *(float*)&bits; } public static string Appended(Action append) { var builder = new StringBuilder("x"); append(builder); return builder.ToString(); } public class LongToStringTests : TestFixture { public void SingleDigits_AreThemselves() { CheckEqual("0", NumberFormatter.LongToString(0)); CheckEqual("7", NumberFormatter.LongToString(7)); CheckEqual("-7", NumberFormatter.LongToString(-7)); } public void NumbersAroundTheStartOfANewDigit_KeepEveryDigit() { CheckEqual("9", NumberFormatter.LongToString(9)); CheckEqual("10", NumberFormatter.LongToString(10)); CheckEqual("99", NumberFormatter.LongToString(99)); CheckEqual("100", NumberFormatter.LongToString(100)); CheckEqual("999999999999999999", NumberFormatter.LongToString(999999999999999999L)); CheckEqual("1000000000000000000", NumberFormatter.LongToString(1000000000000000000L)); CheckEqual("-1000000000000000000", NumberFormatter.LongToString(-1000000000000000000L)); } public void OrdinaryNumbers_AreWrittenInFull() { CheckEqual("12345", NumberFormatter.LongToString(12345)); CheckEqual("-12345", NumberFormatter.LongToString(-12345)); CheckEqual("2147483647", NumberFormatter.LongToString(int.MaxValue)); CheckEqual("-2147483648", NumberFormatter.LongToString(int.MinValue)); } public void TheExtremes_AreWrittenInFull() { CheckEqual("9223372036854775807", NumberFormatter.LongToString(long.MaxValue)); CheckEqual("-9223372036854775808", NumberFormatter.LongToString(long.MinValue)); } public void LongestNumber_IsAsLongAsTheMaximum() { CheckEqual(NumberFormatter.MaxLongLength, NumberFormatter.LongToString(long.MinValue).Length); } public void AnyCulture_GivesTheSameText() { CultureInfo previous = Thread.CurrentThread.CurrentCulture; Thread.CurrentThread.CurrentCulture = new CultureInfo("sv-SE"); try { CheckEqual("-1234567", NumberFormatter.LongToString(-1234567)); } finally { Thread.CurrentThread.CurrentCulture = previous; } } [AllocationTest] public void Number_AllocatesOnlyItsText() { string text = NumberFormatter.LongToString(-1234567); CheckAllocationsAtMost(maxBytesPerRun: IntPtr.Size == 4 ? 32 : 48); CheckEqual("-1234567", text); } } public class WriteLongTests : TestFixture { public void Number_WritesItsLengthAndNothingBeyond() { var buffer = new char[40]; for (int i = 0; i < buffer.Length; ++i) { buffer[i] = '?'; } int length; fixed (char* start = buffer) { length = NumberFormatter.WriteLong(start + 5, -1234567); } CheckEqual(8, length); CheckEqual(new string('?', 5) + "-1234567" + new string('?', 27), new string(buffer)); } [AllocationTest] public void Number_AllocatesNothing() { int length; fixed (char* start = Buffer) { length = NumberFormatter.WriteLong(start, long.MinValue); } CheckNoAllocations(); CheckEqual(20, length); } public static readonly char[] Buffer = new char[NumberFormatter.MaxLongLength]; } public class AppendLongTests : TestFixture { public void Number_GoesAfterWhatIsThere() { CheckEqual("x-42", Appended(builder => NumberFormatter.AppendLong(builder, -42))); } [AllocationTest] public void Number_AllocatesNothing() { Builder.Length = 0; NumberFormatter.AppendLong(Builder, long.MaxValue); CheckNoAllocations(); CheckEqual("9223372036854775807", Builder.ToString()); } } public class DoubleToStringTests : TestFixture { public void WholeNumbers_HaveNoPoint() { CheckEqual("1", NumberFormatter.DoubleToString(1)); CheckEqual("-1", NumberFormatter.DoubleToString(-1)); CheckEqual("100", NumberFormatter.DoubleToString(100)); CheckEqual("1000000000000000", NumberFormatter.DoubleToString(1e15)); CheckEqual("9007199254740992", NumberFormatter.DoubleToString(9007199254740992.0)); CheckEqual("9007199254740994", NumberFormatter.DoubleToString(9007199254740994.0)); } public void Fractions_HaveTheFewestDigitsThatReadBackTheSame() { CheckEqual("1.5", NumberFormatter.DoubleToString(1.5)); CheckEqual("0.5", NumberFormatter.DoubleToString(0.5)); CheckEqual("0.1", NumberFormatter.DoubleToString(0.1)); CheckEqual("4.35", NumberFormatter.DoubleToString(4.35)); CheckEqual("12345.6789", NumberFormatter.DoubleToString(12345.6789)); CheckEqual("123456789.125", NumberFormatter.DoubleToString(123456789.125)); CheckEqual("3.141592653589793", NumberFormatter.DoubleToString(Math.PI)); CheckEqual("2.718281828459045", NumberFormatter.DoubleToString(Math.E)); } public void ValuesThatAreNotExactDecimals_NeedSeventeenDigitsOrFewer() { CheckEqual("0.30000000000000004", NumberFormatter.DoubleToString(0.1 + 0.2)); CheckEqual("0.3333333333333333", NumberFormatter.DoubleToString(1.0 / 3.0)); CheckEqual("0.6666666666666666", NumberFormatter.DoubleToString(2.0 / 3.0)); CheckEqual("0.10000000149011612", NumberFormatter.DoubleToString(0.1f)); } public void Zero_KeepsItsSign() { CheckEqual("0", NumberFormatter.DoubleToString(0.0)); CheckEqual("-0", NumberFormatter.DoubleToString(-0.0)); } public void ValuesWithNoDigits_AreNamed() { CheckEqual("NaN", NumberFormatter.DoubleToString(double.NaN)); CheckEqual("Infinity", NumberFormatter.DoubleToString(double.PositiveInfinity)); CheckEqual("-Infinity", NumberFormatter.DoubleToString(double.NegativeInfinity)); } public void LargeValues_AreWrittenInFullUpToTwentyOneDigits() { CheckEqual("100000000000000000000", NumberFormatter.DoubleToString(1e20)); CheckEqual("123456789012345680000", NumberFormatter.DoubleToString(123456789012345678901.0)); CheckEqual("1152921504606847000", NumberFormatter.DoubleToString(1152921504606846976.0)); } public void ValuesFromAHundredBillionBillion_UseAnExponent() { CheckEqual("1E+21", NumberFormatter.DoubleToString(1e21)); CheckEqual("1.5E+21", NumberFormatter.DoubleToString(1.5e21)); CheckEqual("8.41E+21", NumberFormatter.DoubleToString(8.41e21)); CheckEqual("1E+23", NumberFormatter.DoubleToString(1e23)); CheckEqual("1.5E+300", NumberFormatter.DoubleToString(1.5e300)); } public void SmallValues_AreWrittenInFullDownToAMillionth() { CheckEqual("0.000001", NumberFormatter.DoubleToString(1e-6)); CheckEqual("0.0000015", NumberFormatter.DoubleToString(1.5e-6)); CheckEqual("0.00005", NumberFormatter.DoubleToString(5e-5)); CheckEqual("0.000123", NumberFormatter.DoubleToString(0.000123)); } public void ValuesBelowAMillionth_UseAnExponent() { CheckEqual("1E-7", NumberFormatter.DoubleToString(1e-7)); CheckEqual("1.5E-7", NumberFormatter.DoubleToString(1.5e-7)); CheckEqual("1E-300", NumberFormatter.DoubleToString(1e-300)); CheckEqual("1.5E-300", NumberFormatter.DoubleToString(1.5e-300)); } public void PowersOfTwo_AreWrittenFromTheirNarrowerGapBelow() { CheckEqual("1.2676506002282294E+30", NumberFormatter.DoubleToString(Math.Pow(2, 100))); CheckEqual("7.888609052210118E-31", NumberFormatter.DoubleToString(Math.Pow(2, -100))); CheckEqual("0.25", NumberFormatter.DoubleToString(0.25)); } public void TheLimits_AreWrittenWithTheirSeventeenDigits() { CheckEqual("1.7976931348623157E+308", NumberFormatter.DoubleToString(double.MaxValue)); CheckEqual("-1.7976931348623157E+308", NumberFormatter.DoubleToString(double.MinValue)); CheckEqual("5E-324", NumberFormatter.DoubleToString(double.Epsilon)); CheckEqual("2.2250738585072014E-308", NumberFormatter.DoubleToString(MinNormal)); CheckEqual("2.225073858507201E-308", NumberFormatter.DoubleToString(MaxSubnormal)); } public void Negatives_HaveTheSameDigitsAsTheirPositives() { CheckEqual("-0.30000000000000004", NumberFormatter.DoubleToString(-(0.1 + 0.2))); CheckEqual("-1E-7", NumberFormatter.DoubleToString(-1e-7)); CheckEqual("-123456789012345680000", NumberFormatter.DoubleToString(-123456789012345678901.0)); } public void LongestOutputs_FitTheMaximum() { int max = NumberFormatter.MaxDoubleLength; CheckTrue(NumberFormatter.DoubleToString(-1.2345678901234567e-6).Length <= max); CheckTrue(NumberFormatter.DoubleToString(-double.MaxValue).Length <= max); CheckTrue(NumberFormatter.DoubleToString(-123456789012345678901.0).Length <= max); } public void AnyCulture_GivesTheSameText() { CultureInfo previous = Thread.CurrentThread.CurrentCulture; Thread.CurrentThread.CurrentCulture = new CultureInfo("de-DE"); try { CheckEqual("-1234.5", NumberFormatter.DoubleToString(-1234.5)); } finally { Thread.CurrentThread.CurrentCulture = previous; } } [AllocationTest] public void Number_AllocatesOnlyItsText() { string text = NumberFormatter.DoubleToString(0.1 + 0.2); CheckAllocationsAtMost(maxBytesPerRun: IntPtr.Size == 4 ? 52 : 64); CheckEqual("0.30000000000000004", text); } } public class WriteDoubleTests : TestFixture { public static readonly char[] Buffer = new char[NumberFormatter.MaxDoubleLength]; public void Number_WritesItsLengthAndNothingBeyond() { var buffer = new char[40]; for (int i = 0; i < buffer.Length; ++i) { buffer[i] = '?'; } int length; fixed (char* start = buffer) { length = NumberFormatter.WriteDouble(start + 3, -1.5e-7); } CheckEqual(7, length); CheckEqual(new string('?', 3) + "-1.5E-7" + new string('?', 30), new string(buffer)); } public void NaN_HasNoSignWhatEverItsBits() { double negativeNaN = BitConverter.Int64BitsToDouble(unchecked((long)0xFFF8000000000000UL)); CheckEqual("NaN", NumberFormatter.DoubleToString(negativeNaN)); } [AllocationTest] public void Number_AllocatesNothing() { int length; fixed (char* start = Buffer) { length = NumberFormatter.WriteDouble(start, double.MaxValue); } CheckNoAllocations(); CheckEqual(23, length); } } public class ULongToStringTests : TestFixture { public void SmallNumbers_AreWrittenInFull() { CheckEqual("0", NumberFormatter.ULongToString(0)); CheckEqual("9", NumberFormatter.ULongToString(9)); CheckEqual("10", NumberFormatter.ULongToString(10)); CheckEqual("12345", NumberFormatter.ULongToString(12345)); } public void NumbersBeyondALong_AreWrittenInFull() { CheckEqual("9223372036854775807", NumberFormatter.ULongToString(9223372036854775807UL)); CheckEqual("9223372036854775808", NumberFormatter.ULongToString(9223372036854775808UL)); CheckEqual("18446744073709551615", NumberFormatter.ULongToString(ulong.MaxValue)); } public void LongestNumber_IsAsLongAsTheMaximum() { CheckEqual(NumberFormatter.MaxLongLength, NumberFormatter.ULongToString(ulong.MaxValue).Length); } public void Number_WritesItsLengthAndNothingBeyond() { var buffer = new char[30]; for (int i = 0; i < buffer.Length; ++i) { buffer[i] = '?'; } int length; fixed (char* start = buffer) { length = NumberFormatter.WriteULong(start + 2, 4000000000UL); } CheckEqual(10, length); CheckEqual(new string('?', 2) + "4000000000" + new string('?', 18), new string(buffer)); } [AllocationTest] public void Appending_AllocatesNothing() { Builder.Length = 0; NumberFormatter.AppendULong(Builder, ulong.MaxValue); CheckNoAllocations(); CheckEqual("18446744073709551615", Builder.ToString()); } } public class FloatToStringTests : TestFixture { public void Fractions_HaveTheFewestDigitsThatReadBackAsTheFloat() { CheckEqual("0.1", NumberFormatter.FloatToString(0.1f)); CheckEqual("0.3", NumberFormatter.FloatToString(0.3f)); CheckEqual("1.5", NumberFormatter.FloatToString(1.5f)); CheckEqual("0.001", NumberFormatter.FloatToString(0.001f)); CheckEqual("123456.79", NumberFormatter.FloatToString(123456.789f)); CheckEqual("1.0000001", NumberFormatter.FloatToString(1.00000012f)); } public void ValuesThatAreNotExactDecimals_NeedNineDigitsOrFewer() { CheckEqual("0.33333334", NumberFormatter.FloatToString(1f / 3f)); CheckEqual("0.6666667", NumberFormatter.FloatToString(2f / 3f)); CheckEqual("3.1415927", NumberFormatter.FloatToString(3.14159274f)); CheckEqual("2.7182817", NumberFormatter.FloatToString(2.7182817f)); } public void WholeNumbers_HaveNoPoint() { CheckEqual("100", NumberFormatter.FloatToString(100f)); CheckEqual("8388608", NumberFormatter.FloatToString(8388608f)); CheckEqual("16777216", NumberFormatter.FloatToString(16777216f)); CheckEqual("16777218", NumberFormatter.FloatToString(16777218f)); CheckEqual("10000000000", NumberFormatter.FloatToString(1e10f)); } public void LayoutChangesAtTheSameSizesAsForADouble() { CheckEqual("0.000001", NumberFormatter.FloatToString(1e-6f)); CheckEqual("1E-7", NumberFormatter.FloatToString(1e-7f)); CheckEqual("1E+21", NumberFormatter.FloatToString(1e21f)); } public void TheLimits_AreWrittenWithTheirShortestDigits() { CheckEqual("3.4028235E+38", NumberFormatter.FloatToString(float.MaxValue)); CheckEqual("-3.4028235E+38", NumberFormatter.FloatToString(float.MinValue)); CheckEqual("1E-45", NumberFormatter.FloatToString(float.Epsilon)); CheckEqual("1.1754944E-38", NumberFormatter.FloatToString(MinNormalFloat)); CheckEqual("1.1754942E-38", NumberFormatter.FloatToString(MaxSubnormalFloat)); } public void ValuesWithNoDigits_AreNamedAndZeroKeepsItsSign() { CheckEqual("NaN", NumberFormatter.FloatToString(float.NaN)); CheckEqual("Infinity", NumberFormatter.FloatToString(float.PositiveInfinity)); CheckEqual("-Infinity", NumberFormatter.FloatToString(float.NegativeInfinity)); CheckEqual("0", NumberFormatter.FloatToString(0f)); CheckEqual("-0", NumberFormatter.FloatToString(-0f)); } public void Float_IsShorterThanItsWideningToADouble() { CheckEqual("0.1", NumberFormatter.FloatToString(0.1f)); CheckEqual("0.10000000149011612", NumberFormatter.DoubleToString(0.1f)); } public void Number_WritesItsLengthAndNothingBeyond() { var buffer = new char[30]; for (int i = 0; i < buffer.Length; ++i) { buffer[i] = '?'; } int length; fixed (char* start = buffer) { length = NumberFormatter.WriteFloat(start + 4, -0.1f); } CheckEqual(4, length); CheckEqual(new string('?', 4) + "-0.1" + new string('?', 22), new string(buffer)); } [AllocationTest] public void Appending_AllocatesNothing() { Builder.Length = 0; NumberFormatter.AppendFloat(Builder, 1f / 3f); CheckNoAllocations(); CheckEqual("0.33333334", Builder.ToString()); } } public class AppendDoubleTests : TestFixture { public void Number_GoesAfterWhatIsThere() { CheckEqual("x0.1", Appended(builder => NumberFormatter.AppendDouble(builder, 0.1))); } [AllocationTest] public void Number_AllocatesNothing() { Builder.Length = 0; NumberFormatter.AppendDouble(Builder, 0.1 + 0.2); CheckNoAllocations(); CheckEqual("0.30000000000000004", Builder.ToString()); } } } }