Created
January 26, 2012 01:44
-
-
Save samcragg/1680386 to your computer and use it in GitHub Desktop.
strtod in C# unit tests.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
namespace SvgParser | |
{ | |
/// <summary> | |
/// Provides methods to convert a string to a double precision floating | |
/// point number. | |
/// </summary> | |
public static class StringToDouble | |
{ | |
/// <summary> | |
/// Extracts a number from the specified string, starting at the | |
/// specified index. | |
/// </summary> | |
/// <param name="input">The string to extract a number from.</param> | |
/// <param name="start"> | |
/// The index of the first character in the string to start converting | |
/// to a number. | |
/// </param> | |
/// <param name="end"> | |
/// When this methods returns, contains the index of the end of the | |
/// extracted number. | |
/// </param> | |
/// <returns>A number representation of the string.</returns> | |
/// <exception cref="ArgumentNullException">input is null.</exception> | |
/// <exception cref="ArgumentOutOfRangeException"> | |
/// start represents an index that is outside of the range for input. | |
/// </exception> | |
public static double Parse(string input, int start, out int end) | |
{ | |
if (input == null) | |
{ | |
throw new ArgumentNullException("input"); | |
} | |
if (start < 0) | |
{ | |
throw new ArgumentOutOfRangeException("start", "Value cannot be negative."); | |
} | |
if (start > input.Length) | |
{ | |
throw new ArgumentOutOfRangeException("start", "Value must be less then input.Length"); | |
} | |
int endOfWhitespace = SkipWhiteSpace(input, start); | |
long significand; | |
int significandsExponent, sign; | |
int startOfDigits = ParseSign(input, endOfWhitespace, out sign); | |
int index = SkipLeadingZeros(input, startOfDigits); | |
index = ParseSignificand(input, index, out significand, out significandsExponent); | |
// Have we parsed a number? | |
if ((index - startOfDigits) > 0) | |
{ | |
int exponent; | |
end = ParseExponent(input, index, out exponent); | |
return MakeDouble(significand * sign, exponent - significandsExponent); | |
} | |
// Not a number, is it a constant? | |
double value; | |
end = ParseNamedConstant(input, endOfWhitespace, out value); | |
if (end != endOfWhitespace) | |
{ | |
return value; | |
} | |
// If we're here then we couldn't parse anything. | |
end = start; | |
return default(double); | |
} | |
private static double MakeDouble(long significand, int exponent) | |
{ | |
// Improve the accuracy of the result for negative exponents. | |
if (exponent < 0) | |
{ | |
// Allow for denormalized numbers (allows values less than emin) | |
if (exponent < -308) // Smallest normalized floating points number | |
{ | |
return (significand / Math.Pow(10, -308 - exponent)) / 1e308; | |
} | |
return significand / Math.Pow(10, -exponent); | |
} | |
return significand * Math.Pow(10, exponent); | |
} | |
private static int ParseExponent(string str, int startIndex, out int exponent) | |
{ | |
exponent = 0; | |
int index = startIndex; | |
if (index < str.Length) | |
{ | |
if ((str[index] == 'e') || (str[index] == 'E')) | |
{ | |
int sign; | |
index = ParseSign(str, index + 1, out sign); // Add one to the index to skip the 'e' | |
int digitStart = index; // Keep a track of if we parse any digits | |
index = SkipLeadingZeros(str, index); | |
long value = 0; | |
index = ParseNumber(3, str, index, ref value); | |
if ((index - digitStart) == 0) | |
{ | |
exponent = 0; | |
return startIndex; // We didn't parse anything | |
} | |
exponent = (int)(value * sign); | |
} | |
} | |
return index; | |
} | |
private static int ParseNamedConstant(string str, int startIndex, out double value) | |
{ | |
// StartsWith only converts str to uppercase - important we pass the constant in uppercase | |
if (StartsWith(str, startIndex, "NAN")) | |
{ | |
value = double.NaN; | |
return startIndex + 3; | |
} | |
// Infinity can be positive or negative | |
int sign; | |
int index = ParseSign(str, startIndex, out sign); | |
if (StartsWith(str, index, "INFINITY")) | |
{ | |
value = sign * double.PositiveInfinity; | |
return index + 8; | |
} | |
if (StartsWith(str, index, "INF")) | |
{ | |
value = sign * double.PositiveInfinity; | |
return index + 3; | |
} | |
// No match | |
value = default(double); | |
return startIndex; | |
} | |
private static int ParseNumber(int maxDigits, string str, int index, ref long value) | |
{ | |
for (; index < str.Length; index++) | |
{ | |
char c = str[index]; | |
if ((c < '0') || (c > '9')) | |
{ | |
break; | |
} | |
if (--maxDigits >= 0) | |
{ | |
value = (value * 10) + (c - '0'); | |
} | |
} | |
return index; | |
} | |
private static int ParseSign(string str, int index, out int value) | |
{ | |
value = 1; // Default to positive | |
if (index < str.Length) | |
{ | |
char c = str[index]; | |
if (c == '-') | |
{ | |
value = -1; | |
return index + 1; | |
} | |
if (c == '+') | |
{ | |
return index + 1; | |
} | |
} | |
return index; | |
} | |
private static int ParseSignificand(string str, int startIndex, out long significand, out int exponent) | |
{ | |
exponent = 0; | |
significand = 0; | |
int index = ParseNumber(18, str, startIndex, ref significand); | |
int digits = index - startIndex; | |
// Is there a decimal part as well? | |
if ((index < str.Length) && (str[index] == '.')) | |
{ | |
int point = ++index; // Skip the decimal point | |
// If there are no significant digits before the decimal point | |
// then skip the zeros after it as well, making sure we adjust | |
// the exponent. | |
// e.g. .0001 == 1e-4 (-3 zeros + -1 decimal digit == -4) | |
if (digits == 0) | |
{ | |
index = SkipLeadingZeros(str, index); | |
} | |
index = ParseNumber(18 - digits, str, index, ref significand); | |
exponent = index - point; | |
// Check it's not just a decimal point | |
if ((index - startIndex) == 1) | |
{ | |
return startIndex; // Didn't parse anything | |
} | |
} | |
return index; | |
} | |
private static int SkipLeadingZeros(string str, int index) | |
{ | |
for (; index < str.Length; index++) | |
{ | |
if (str[index] != '0') | |
{ | |
break; | |
} | |
} | |
return index; | |
} | |
private static int SkipWhiteSpace(string str, int index) | |
{ | |
for (; index < str.Length; index++) | |
{ | |
char c = str[index]; | |
if ((c != '\x09') && | |
(c != '\x0D') && | |
(c != '\x0A') && | |
(c != '\x20')) | |
{ | |
break; | |
} | |
} | |
return index; | |
} | |
private static bool StartsWith(string str, int startIndex, string value) | |
{ | |
for (int i = 0; i < value.Length; i++) | |
{ | |
if ((startIndex + i) >= str.Length) | |
{ | |
return false; | |
} | |
if (char.ToUpperInvariant(str[startIndex + i]) != value[i]) | |
{ | |
return false; | |
} | |
} | |
return true; | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using NUnit.Framework; | |
namespace SvgParser | |
{ | |
[TestFixture] | |
public sealed class StringToDoubleTest | |
{ | |
[Test] | |
public void TestEmptyAndNullStrings() | |
{ | |
int end = 1; | |
Assert.That(() => StringToDouble.Parse(null, 0, out end), | |
Throws.TypeOf<ArgumentNullException>()); | |
Assert.That(end, Is.EqualTo(1)); // Make sure end wasn't touched | |
Assert.That(StringToDouble.Parse(string.Empty, 0, out end), Is.EqualTo(default(double))); | |
Assert.That(end, Is.EqualTo(0)); | |
} | |
[Test] | |
public void TestInvalidStartIndex() | |
{ | |
int end = 1; | |
Assert.That(() => StringToDouble.Parse(string.Empty, 1, out end), | |
Throws.TypeOf<ArgumentOutOfRangeException>()); | |
Assert.That(end, Is.EqualTo(1)); // Make sure end wasn't touched | |
Assert.That(() => StringToDouble.Parse(string.Empty, -1, out end), | |
Throws.TypeOf<ArgumentOutOfRangeException>()); | |
Assert.That(end, Is.EqualTo(1)); // Make sure end wasn't touched | |
} | |
[Test] | |
public void TestValidFormats() | |
{ | |
TestCompleteParse("1", 1.0); | |
TestCompleteParse(".2", 0.2); | |
TestCompleteParse("1e1", 10.0); | |
TestCompleteParse(".2e1", 2.0); | |
TestCompleteParse("1.2e1", 12.0); | |
TestCompleteParse("+1", 1.0); | |
TestCompleteParse("+.2", 0.2); | |
TestCompleteParse("+1e1", 10.0); | |
TestCompleteParse("+.2e1", 2.0); | |
TestCompleteParse("+1.2e1", 12.0); | |
TestCompleteParse("+1e+1", 10.0); | |
TestCompleteParse("+.2e+1", 2.0); | |
TestCompleteParse("+1.2e+1", 12.0); | |
} | |
[Test] | |
public void TestValidRange() | |
{ | |
TestCompleteParse("+0", 0.0); | |
TestCompleteParse("+4.9406564584124654E-324", double.Epsilon); | |
TestCompleteParse("+1.7976931348623157E+308", double.MaxValue); | |
TestCompleteParse("-0", -0.0); | |
TestCompleteParse("-4.9406564584124654E-324", -double.Epsilon); | |
TestCompleteParse("-1.7976931348623157E+308", double.MinValue); | |
} | |
[Test] | |
public void TestInfinityAndNaN() | |
{ | |
TestCompleteParse("+infinity", double.PositiveInfinity); | |
TestCompleteParse("-INF", double.NegativeInfinity); | |
TestCompleteParse("NAN", double.NaN); | |
} | |
[Test] | |
public void TestOutsideOfRange() | |
{ | |
TestCompleteParse("+1E-325", 0.0); | |
TestCompleteParse("+1E+309", double.PositiveInfinity); | |
TestCompleteParse("-1E-325", 0.0); | |
TestCompleteParse("-1E+309", double.NegativeInfinity); | |
} | |
[Test] | |
public void TestInvalidFormats() | |
{ | |
// Put the spaces at the start to make sure it doesn't skip anything. | |
var formats = new[] | |
{ | |
" .", | |
" ++0", | |
" +" | |
}; | |
foreach (var format in formats) | |
{ | |
int position; | |
double value = StringToDouble.Parse(format, 0, out position); | |
Assert.That(value, Is.EqualTo(default(double))); | |
Assert.That(position, Is.EqualTo(0)); // Make sure it read nothing | |
} | |
} | |
[Test] | |
public void TestPartiallyValid() | |
{ | |
var formats = new[] | |
{ | |
" 1e", | |
" 1e.0", | |
"1.e", | |
" 1e++0", | |
" 1ee0" | |
}; | |
foreach (var format in formats) | |
{ | |
int position; | |
double value = StringToDouble.Parse(format, 0, out position); | |
Assert.That(value, Is.EqualTo(1.0)); | |
Assert.That(position, Is.EqualTo(2)); // Make sure it read as much as it could | |
} | |
} | |
[Test] | |
public void TestStartIndex() | |
{ | |
var formats = new[] | |
{ | |
"###0.00###", | |
"###+0.0###", | |
"###-0.0###", | |
"###+0e0###", | |
"###-0e0###", | |
"###0e-0###", | |
"###.0e0.##", | |
}; | |
foreach (var format in formats) | |
{ | |
int position; | |
double value = StringToDouble.Parse(format, 3, out position); | |
Assert.That(value, Is.EqualTo(0.0)); | |
Assert.That(position, Is.EqualTo(7)); // Make sure it read as much as it could | |
} | |
} | |
private static void TestCompleteParse(string input, double expected) | |
{ | |
int position = 0; | |
double parsed = StringToDouble.Parse(input, 0, out position); | |
Assert.That(parsed, Is.EqualTo(expected)); | |
Assert.That(position, Is.EqualTo(input.Length)); // Make sure it read everything | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment