Skip to content

Instantly share code, notes, and snippets.

@samcragg
Created January 26, 2012 01:44
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save samcragg/1680386 to your computer and use it in GitHub Desktop.
Save samcragg/1680386 to your computer and use it in GitHub Desktop.
strtod in C# unit tests.
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;
}
}
}
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