using System.Globalization;
using System.Collections.Generic;
using System.Diagnostics;
using Microsoft.VisualStudio.TestTools.UnitTesting;
public static class Parsers
private static readonly CultureInfo CurrentCulture = CultureInfo.CurrentCulture;
private static readonly NumberFormatInfo NumberFormat = CurrentCulture.NumberFormat;
private static readonly char CharNegative = NumberFormat.NegativeSign[0];
public static unsafe bool FastTryParseInt(string input, out int result)
fixed (char* cString = input)
char* nextChar = cString;
if (*nextChar == CharNegative)
while (*nextChar >= '0' && *nextChar <= '9')
result = result * 10 + (*nextChar++ - '0');
if (*nextChar != Char.MinValue) return false;
long ptrLen = nextChar - cString;
return ptrLen < 11L || ptrLen <= 11L && result <= 0;
return ptrLen < 10L || ptrLen <= 10L && result >= 0;
#region Parser Test Helpers
private delegate void ParserTestFunction<in T>(string value, T parsedResult);
private delegate void EqualityAsserter<in T>(T expected, T actual, string message);
private ParserTestFunction<T> ParserTestSuccessFactory<T>(
ParserFunction<T>.Custom fastParse,
ParserFunction<T>.Custom parserWrapper,
ParserTestFunction<T> compareToNativeParser,
EqualityAsserter<T> equalityTest) =>
Assert.IsTrue(fastParse(value, out T result),
$"Expected Custom Parser to be able to handle the input \"{value}\"");
equalityTest(expected, result,
$"The input string \"{value}\" was not parsed correctly.");
compareToNativeParser(value, result);
Assert.IsTrue(parserWrapper(value, out T result2));
Assert.AreEqual(result2, result, "Expected identical outputs " +
$"from both signatures when parsing the string \"{value}\".");
private ParserTestFunction<T> ParserTestFallbackFactory<T>(
ParserFunction<T>.Custom fastParse,
ParserFunction<T>.Custom parserWrapper,
ParserTestFunction<T> compareToNativeParser,
EqualityAsserter<T> equalityTest) =>
Assert.IsFalse(fastParse(value, out T result),
$"Expected The fast custom parser to be unable to handle the input \"{value}\"");
Assert.IsTrue(parserWrapper(value, out result),
$"Expected the custom parser wrapper to be able to handle the input \"{value}\"");
equalityTest(expected, result, "Parsed result was not as expected.");
compareToNativeParser(value, result);
private Action<string> ParserTestUnparsableFactory<T>(ParserFunction<T>.Custom fastParse,
ParserFunction<T>.Custom parserWrapper) => value =>
Assert.IsFalse(fastParse(value, out T result),
$"Expected the fast custom parser to be unable to handle the input \"{value}\". " +
$"Instead, it parsed the result as {result:G17}");
Assert.IsFalse(parserWrapper(value, out result),
$"Expected the custom parser wrapper to be unable to handle the input \"{value}\". " +
$"Instead, it parsed the result as {result:G17}");
#endregion Parser Test Helper Delegates
[TestMethod, TestCategory(TestCategory)]
public void Test_Utilities_Parsers_ParseInt()
void CompareToNativeParser(string value, int parsedResult)
if (!Int32.TryParse(value, NumberStyles.Currency | NumberStyles.AllowExponent,
CultureInfo.CurrentCulture, out int nativeResult))
Console.WriteLine($"Note: Native parser cannot parse the string \"{value}\".");
Assert.AreEqual(nativeResult, parsedResult, "Expected output to be identical " +
$"to the native parser when parsing the string \"{value}\".");
ParserTestFunction<int> TestSuccess = ParserTestSuccessFactory<int>(
Parsers.FastTryParseInt, Parsers.TryParseInt, CompareToNativeParser, Assert.AreEqual);
TestSuccess("123456789", 123456789);
TestSuccess("-123456789", -123456789);
TestSuccess("000012345", 12345);
TestSuccess("-000012345", -12345);
TestSuccess("123450000", 123450000);
TestSuccess("-123450000", -123450000);
TestSuccess("1000000000", 1000000000);
TestSuccess("-1000000000", -1000000000);
TestSuccess("2147483647", 2147483647);
TestSuccess("-2147483648", -2147483648);
ParserTestFunction<int> TestFallback = ParserTestFallbackFactory<int>(
Parsers.FastTryParseInt, Parsers.TryParseInt, CompareToNativeParser, Assert.AreEqual);
TestFallback("1.2345678e7", 12345678);
TestFallback("-1.2345678e7", -12345678);
TestFallback("1,234,567,890", 1234567890);
TestFallback("-1,234,567,890", -1234567890);
TestFallback("Unlimited", Int32.MaxValue);
TestFallback("∞", Int32.MaxValue);
TestFallback("-∞", Int32.MinValue);
TestFallback("$123", 123);
TestFallback("123$", 123);
TestFallback("£123", 123);
TestFallback("€123", 123);
TestFallback("¥123", 123);
TestFallback("₱123", 123);
TestFallback("$ 123", 123);
TestFallback("123 $", 123);
TestFallback(" 1234", 1234);
TestFallback("1234 ", 1234);
TestFallback(" 1234 ", 1234);
TestFallback(" -1234", -1234);
TestFallback("- 1234", -1234);
Action<string> TestUnparsable = ParserTestUnparsableFactory<int>(
Parsers.FastTryParseInt, Parsers.TryParseInt);
TestUnparsable("2147483648");
TestUnparsable("-2147483649");
TestUnparsable("4294967296");
TestUnparsable("-4294967296");
TestUnparsable("9999999999");
TestUnparsable("-9999999999");
TestUnparsable("10000000000");
TestUnparsable("-10000000000");
TestUnparsable("1000000000000000000000000000000");
TestUnparsable("-1000000000000000000000000000000");
TestUnparsable(@"1234a");
TestUnparsable(@"123.456.789");
TestUnparsable(@"12 % 89");
TestUnparsable(@"--123");
TestUnparsable(@"-123-");
TestUnparsable("-0.00000000000000000123456789~");
[TestMethod, TestCategory(TestCategory)]
public void Test_Utilities_Parsers_FastParseInt_Performance()
const int testRange = 1000000000;
Random random = new Random();
PerformanceTestHelper<int>(() => random.Next(0, testRange).ToString(),
Int32.TryParse, Parsers.FastTryParseInt, NumberStyles.AllowLeadingSign);
private static class ParserFunction<T>
public delegate bool Native(string value, NumberStyles style, CultureInfo info, out T result);
public delegate bool Custom(string value, out T result);
private void PerformanceTestHelper<T>(Func<string> generateRandomInput,
ParserFunction<T>.Native nativeParse, ParserFunction<T>.Custom customParse,
NumberStyles nativeNumberStyles)
const double requiredGain = 0.25;
const double requiredGain = 1d;
const int uniqueValues = 1000000;
const int iterations = 100;
Stopwatch timer = Stopwatch.StartNew();
CultureInfo cachedCulture = CultureInfo.CurrentCulture;
nativeParse(generateRandomInput(), nativeNumberStyles, cachedCulture, out T _);
customParse(generateRandomInput(), out T _);
Console.WriteLine($"Took {timer.ElapsedMilliseconds} ms to initialize parsers.");
string[] randomIntegers = Enumerable.Repeat(0, uniqueValues)
.Select(i => generateRandomInput()).ToArray();
Console.WriteLine($"Took {timer.ElapsedMilliseconds} ms to generate random strings.");
foreach (int _ in Enumerable.Repeat(0, iterations))
foreach (string value in randomIntegers)
if (!nativeParse(value, nativeNumberStyles, cachedCulture, out T _))
Assert.Fail($"Native parser failed to parse the string {value}");
double nativeElapsed = timer.ElapsedMilliseconds;
Console.WriteLine($"Native parser took {nativeElapsed} ms.");
foreach (int _ in Enumerable.Repeat(0, iterations))
foreach (string value in randomIntegers)
if (!customParse(value, out T _))
Assert.Fail($"Custom parser failed to parse the string {value}");
double customElapsed = timer.ElapsedMilliseconds;
Console.WriteLine($"Custom parser took {customElapsed} ms.");
double perfGain = nativeElapsed / customElapsed - 1;
Console.WriteLine($"Performance gain was {perfGain:P2}");
if (perfGain < requiredGain)
string message = $"Expected a {requiredGain:P2} improvement in parsing performance " +
$"or better by using a custom parser, but the performance difference was {perfGain:P2}.";
Assert.Inconclusive(message);