Skip to content

Instantly share code, notes, and snippets.

@Petrusion
Last active April 26, 2022 00:28
Show Gist options
  • Save Petrusion/a33fbb0f2e6780cbc29854a1a5229625 to your computer and use it in GitHub Desktop.
Save Petrusion/a33fbb0f2e6780cbc29854a1a5229625 to your computer and use it in GitHub Desktop.
Quickly thrown together Interpolated String Handler based Parser
using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace ParserTest;
public static class Parser
{
[InterpolatedStringHandler]
[StructLayout(LayoutKind.Auto)]
public ref struct ParseValuesHandler
{
private bool successfulSoFar = true;
private int parsedValuesCount = 0;
private readonly int valuesToParseCount;
private ReadOnlySpan<char> charSource;
private readonly IFormatProvider? formatProvider;
internal bool Successful => successfulSoFar;
internal int ParsedValuesCount => parsedValuesCount;
private bool PointlessToContinue => !successfulSoFar | parsedValuesCount >= valuesToParseCount;
public ParseValuesHandler(int literalLength, int formattedCount, ReadOnlySpan<char> parsedChars, IFormatProvider? formatProvider = null)
{
this.charSource = parsedChars;
this.valuesToParseCount = formattedCount;
this.formatProvider = formatProvider;
}
public void AppendLiteral(string s)
{
if (PointlessToContinue)
{
return;
}
int literalIndex = charSource.IndexOf(s);
if (literalIndex < 0)
{
successfulSoFar = false;
return;
}
charSource = charSource[(literalIndex + s.Length)..];
}
public void AppendFormatted<T>(in T valueToParse) where T : ISpanParseable<T>
{
if (PointlessToContinue)
{
return;
}
if (parsedValuesCount != valuesToParseCount - 1)
{
throw new InvalidOperationException("Only the last parsed value is allowed not to have length or separator defined");
}
AppendFormatted(valueToParse, charSource.Length);
}
public void AppendFormatted<T>(in T valueToParse, string format) where T : ISpanParseable<T>
{
string separator = format ?? throw new ArgumentNullException(nameof(format));
if (PointlessToContinue)
{
return;
}
int separatorIndex = charSource.IndexOf(separator);
if (separatorIndex <= 0)
{
successfulSoFar = false;
return;
}
AppendFormatted(valueToParse, separatorIndex);
if (PointlessToContinue)
{
return;
}
charSource = charSource[separator.Length..];
}
public void AppendFormatted<T>(in T valueToParse, int alignment) where T : ISpanParseable<T>
{
int length = alignment;
if (length <= 0)
{
throw new ArgumentException("The length of parsed value must be positive", nameof(alignment));
}
if (PointlessToContinue)
{
return;
}
if (T.TryParse(charSource[..length], formatProvider, out Unsafe.AsRef(valueToParse)))
{
charSource = charSource[length..];
parsedValuesCount++;
}
else
{
successfulSoFar = false;
}
}
}
[InterpolatedStringHandler]
[StructLayout(LayoutKind.Auto)]
public ref struct ParseHomogenousValuesHandler<TParseable> where TParseable : ISpanParseable<TParseable>
{
private bool successfulSoFar = true;
private int parsedValuesCount = 0;
private readonly int valuesToParseCount;
private int separatorLengthSoFar = 0;
private readonly int totalSeparatorLength;
private ReadOnlySpan<char> charSource;
private readonly IFormatProvider? formatProvider;
private Span<TParseable> nextValueToParse = default;
internal bool Successful => successfulSoFar;
internal int ParsedValuesCount => parsedValuesCount;
private bool PointlessToContinue => !successfulSoFar | parsedValuesCount >= valuesToParseCount;
public ParseHomogenousValuesHandler(int literalLength, int formattedCount, ReadOnlySpan<char> parsedChars, IFormatProvider? formatProvider = null)
{
this.charSource = parsedChars;
this.valuesToParseCount = formattedCount;
this.totalSeparatorLength = literalLength;
this.formatProvider = formatProvider;
}
public void AppendLiteral(string s)
{
if (PointlessToContinue)
{
return;
}
separatorLengthSoFar += s.Length;
int literalIndex = charSource.IndexOf(s);
if (literalIndex < 0)
{
successfulSoFar = false;
return;
}
if (!nextValueToParse.IsEmpty)
{
if (TParseable.TryParse(charSource[..literalIndex], formatProvider, out MemoryMarshal.GetReference(nextValueToParse)))
{
parsedValuesCount++;
}
else
{
successfulSoFar = false;
return;
}
nextValueToParse = default;
}
charSource = charSource[(literalIndex + s.Length)..];
}
public void AppendFormatted(in TParseable valueToParse)
{
if (PointlessToContinue)
{
return;
}
if (!nextValueToParse.IsEmpty)
{
throw new InvalidOperationException("Separators between values are needed");
}
if (parsedValuesCount == valuesToParseCount - 1 && separatorLengthSoFar >= totalSeparatorLength)
{
// if this is the last value to parse but there isn't any literals (separators) left, just format
// using the rest of charSource
if (TParseable.TryParse(charSource, formatProvider, out Unsafe.AsRef(valueToParse)))
{
parsedValuesCount++;
}
else
{
successfulSoFar = false;
return;
}
}
else
{
// save the reference to valueToParse so that AppendLiteral can use it
nextValueToParse = MemoryMarshal.CreateSpan(ref Unsafe.AsRef(valueToParse), 1);
}
}
}
public static bool TryParseValues(
ReadOnlySpan<char> source,
[InterpolatedStringHandlerArgument("source")] ref ParseValuesHandler handler,
out int parsedValuesCount)
{
parsedValuesCount = handler.ParsedValuesCount;
return handler.Successful;
}
public static bool TryParseValues(
ReadOnlySpan<char> source,
IFormatProvider? formatProvider,
[InterpolatedStringHandlerArgument("source", "formatProvider")] ref ParseValuesHandler handler,
out int parsedValuesCount)
{
parsedValuesCount = handler.ParsedValuesCount;
return handler.Successful;
}
public static void ParseValues(
ReadOnlySpan<char> source,
[InterpolatedStringHandlerArgument("source")] ref ParseValuesHandler handler)
{
if (!handler.Successful)
{
throw new ArgumentException("Parsing unsuccessful");
}
}
public static void ParseValues(
ReadOnlySpan<char> source,
IFormatProvider? formatProvider,
[InterpolatedStringHandlerArgument("source", "formatProvider")] ref ParseValuesHandler handler)
{
if (!handler.Successful)
{
throw new ArgumentException("Parsing unsuccessful");
}
}
public static bool TryParseHomogenousValues<T>(
ReadOnlySpan<char> source,
[InterpolatedStringHandlerArgument("source")] ref ParseHomogenousValuesHandler<T> handler,
out int parsedValuesCount)
where T : ISpanParseable<T>
{
parsedValuesCount = handler.ParsedValuesCount;
return handler.Successful;
}
public static bool TryParseHomogenousValues<T>(
ReadOnlySpan<char> source,
IFormatProvider? formatProvider,
[InterpolatedStringHandlerArgument("source", "formatProvider")] ref ParseHomogenousValuesHandler<T> handler,
out int parsedValuesCount)
where T : ISpanParseable<T>
{
parsedValuesCount = handler.ParsedValuesCount;
return handler.Successful;
}
public static void ParseHomogenousValues<T>(
ReadOnlySpan<char> source,
[InterpolatedStringHandlerArgument("source")] ref ParseHomogenousValuesHandler<T> handler)
where T : ISpanParseable<T>
{
if (!handler.Successful)
{
throw new ArgumentException("Parsing unsuccessful");
}
}
public static void ParseHomogenousValues<T>(
ReadOnlySpan<char> source,
IFormatProvider? formatProvider,
[InterpolatedStringHandlerArgument("source", "formatProvider")] ref ParseHomogenousValuesHandler<T> handler)
where T : ISpanParseable<T>
{
if (!handler.Successful)
{
throw new ArgumentException("Parsing unsuccessful");
}
}
}
using System;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using ParserTest;
string versionString = "major and minor: 6 and 0 ... build and revision are 100, 7";
int major = 0, minor = 0, build = 0, revision = 0;
// here, format is used to inform the parser which chars should be after the value,
// and string literals inbetween are used to inform the parser what chars should be behind each value
Parser.ParseValues(versionString,
$"minor: {major: and} {minor: .} are {build:,} {revision}");
Console.WriteLine($"{major}.{minor}.{build}.{revision}");
// prints "6.0.100.7"
major = minor = build = revision = 0;
// here, format is not used but entire substrings between values
// have to be specified
Parser.ParseHomogenousValues<int>(versionString,
$"minor: {major} and {minor} ... build and revision are {build}, {revision}");
Console.WriteLine($"{major}.{minor}.{build}.{revision}");
// prints "6.0.100.7"
// mm_YYYY_dd:hh for some reason
string awkwardDateString = "04_2022_23:20";
byte hour = 0, day = 0, month = 0;
int year = 0;
//here, alignment is used to infrom the parser how long each substring is
//this method supports multiple types, so we can use both byte and int
Parser.ParseValues(awkwardDateString, $"{month,2}_{year,4}_{day,2}:{hour,2}");
DateTime date = new(year, month, day, hour, 0, 0);
Console.WriteLine(date.ToString(CultureInfo.InvariantCulture));
// prints 04/23/2022 20:00:00
month = day = hour = 0;
year = 0;
//or if we can't guarantee length of substrings:
Parser.ParseValues(awkwardDateString, $"{month:_}{year:_}{day::}{hour}");
date = new(year, month, day, hour, 0, 0);
Console.WriteLine(date.ToString(CultureInfo.InvariantCulture));
// prints 04/23/2022 20:00:00
year = 0;
int iHour = 0, iDay = 0, iMonth = 0;
//with this method, we can't use ints AND bytes, only one T at a time
Parser.ParseHomogenousValues<int>(awkwardDateString,
$"{iMonth}_{year}_{iDay}:{iHour}");
date = new(year, month, day, hour, 0, 0);
Console.WriteLine(date.ToString(CultureInfo.InvariantCulture));
// prints 04/23/2022 20:00:00
string someNumbers = "10, 20, 30, 65.984, 8965.1233121231";
int a = 0, b = 0, c = 0;
float e = 0;
double f = 0;
// IFormatProvider can be supplied
Parser.ParseValues(someNumbers, CultureInfo.InvariantCulture,
$"{a:,} {b:,} {c,2} {e:,} {f}");
Console.WriteLine($"{a + b + c}...{e + f}");
//prints "60...9031,107313282768" on my machine
double v = 0, w = 0, x = 0, y = 0, z = 0;
// can use the 'homogenous' parse method, but the variables
// have to be of the same type
Parser.ParseHomogenousValues<double>(someNumbers, CultureInfo.InvariantCulture,
$"{v}, {w}, {x}, {y}, {z}");
Console.WriteLine($"{v + w + x}...{y + z}");
//prints "60...9031,1073121231" on my machine
string vectorsString =
"( 10.5 ; 9e02 ; 98.3 )\n" +
"( 9999 ; 1111 ; 2e8 )\n" +
"( 3255 ; .1e6 ; 48.987 )\n";
var vectorsArray = new Vector3[3];
Parser.ParseHomogenousValues<Vector3>(vectorsString, CultureInfo.InvariantCulture,
$"{vectorsArray[0]}\n{vectorsArray[1]}\n{vectorsArray[2]}");
foreach (var vec in vectorsArray)
{
Console.WriteLine(vec);
}
/*
prints
( 10,5 ; 900 ; 98,3 )
( 9999 ; 1111 ; 200000000 )
( 3255 ; 100000 ; 48,987 )
*/
struct Vector3 : ISpanParseable<Vector3>
{
private float x, y, z;
public static Vector3 Parse(ReadOnlySpan<char> s, IFormatProvider? provider)
{
throw new NotImplementedException();
}
public static Vector3 Parse(string s, IFormatProvider? provider)
{
throw new NotImplementedException();
}
public static bool TryParse(ReadOnlySpan<char> s, IFormatProvider? provider, out Vector3 result)
{
result = new Vector3();
ref float
x = ref result.x,
y = ref result.y,
z = ref result.z;
return Parser.TryParseHomogenousValues<float>(s, provider,
$"( {x} ; {y} ; {z} )", out _);
}
public static bool TryParse([NotNullWhen(true)] string? s, IFormatProvider? provider, out Vector3 result)
{
throw new NotImplementedException();
}
public override string ToString() => $"( {x} ; {y} ; {z} )";
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment