Tokenisation/NumericTokenExtractor.cs
using System.Globalization;
namespace Expressive.Tokenisation
{
internal class NumericTokenExtractor : ITokenExtractor
{
public Token ExtractToken(string expression, int currentIndex, Context context)
{
var character = expression[currentIndex];
if (!IsValidStart(character, context, NumberStyles.Any))
{
return null;
}
var location = GetNumberLocation(expression, currentIndex, context, NumberStyles.Any);
return new Token(expression.Substring(location.Start, location.Length), currentIndex);
}
private static Location GetNumberLocation(string expression, int startIndex, Context context, NumberStyles numberStyles)
{
var index = startIndex;
var character = expression[index];
var expressionLength = expression.Length;
while (IsAllowableCharacter(character, expression, index, startIndex, expressionLength, context, ref numberStyles, out var exponentialLocation) &&
index < expressionLength)
{
if (exponentialLocation != null)
{
return new Location(startIndex, exponentialLocation.End);
}
index++;
if (index == expression.Length)
{
break;
}
character = expression[index];
}
return new Location(startIndex, index);
}
private static bool IsAllowableCharacter(
char character,
string expression,
int index,
int startIndex,
int expressionLength,
Context context,
ref NumberStyles numberStyles,
out Location exponentialLocation)
{
exponentialLocation = null;
if (char.IsDigit(character) || IsValidStart(character, context, numberStyles) && index == startIndex)
{
return true;
}
// No more exponential or decimal chars after our first exponential.
if (!numberStyles.HasFlag(NumberStyles.AllowExponent))
{
return false;
}
// If we find an exponential character then carry on to find a valid integer.
if ((character == 'e' || character == 'E') &&
char.IsDigit(expression[index - 1]) && // make sure the e is preceded by an actual number.
index + 1 < expressionLength &&
IsValidStart(expression[index + 1], context, NumberStyles.Integer))
{
exponentialLocation = GetNumberLocation(expression, index + 1, context, NumberStyles.Integer);
if (exponentialLocation != null)
{
// No need to interact with the numberStyles as supplying the location short circuits.
return true;
}
}
if (numberStyles.HasFlag(NumberStyles.AllowDecimalPoint) && character == context.DecimalSeparator)
{
numberStyles &= ~NumberStyles.AllowDecimalPoint;
return true;
}
return false;
}
private static bool IsSignCharacter(char character) => character == '-' || character == '\u2212' || character == '+';
private static bool IsValidStart(char character, Context context, NumberStyles numberStyles) =>
char.IsDigit(character) ||
numberStyles.HasFlag(NumberStyles.AllowLeadingSign) && IsSignCharacter(character) ||
numberStyles.HasFlag(NumberStyles.AllowDecimalPoint) && character == context.DecimalSeparator;
private class Location
{
public int End { get; }
public int Length => End - Start;
public int Start { get; }
public Location(int start, int end)
{
Start = start;
End = end;
}
}
}
}