-
-
Save fjod/55a8a4bd34a8a43390b9d5d9f4dc377d to your computer and use it in GitHub Desktop.
Quickly thrown together Interpolated String Handler based Parser
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 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"); | |
} | |
} | |
} |
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 System.Diagnostics.CodeAnalysis; | |
using System.Globalization; | |
using ParserTest; | |
string versionString = "minor and major: 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, | |
$"major: {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, | |
$"major: {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
https://www.reddit.com/r/csharp/comments/uacz1s/this_is_really_cool_in_c_11_thanks_to_static/