Last active
January 4, 2024 07:39
-
-
Save t-mat/b026bd24274c13bf09cff1c623a2e4aa to your computer and use it in GitHub Desktop.
[C#]Simple ReadOnlySpan<char> .ini parser
This file contains 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
// Simple ReadOnlySpan<char> .ini parser | |
// | |
// License | |
// ------- | |
// SPDX-FileCopyrightText: Copyright (c) Takayuki Matsuoka | |
// SPDX-License-Identifier: CC0-1.0 | |
// | |
using System; | |
namespace IniParser { | |
/// <summary> | |
/// Simple .ini file reader using ReadOnlySpan<char> instead of string. | |
/// </summary> | |
/// <example> | |
/// <code> | |
/// const string testStr = | |
/// "K0=V0\n" + // L#1, S="", K="K0", V="V0" | |
/// "[S1]\n" + // L#2, S="S1", K="", V="" | |
/// "K1=V1\n" + // L#3, S="S1", K="K1", V="V1" | |
/// "K2\n" + // L#4, S="S1", k+"K2" | |
/// "\n" + // skip | |
/// "[S2]\n" + // L#6, S="S2", K="", V="" | |
/// "K3=V3\n" ; // L#7, S="S2", K=K3", V="V3" | |
/// | |
/// IniParser iniParser = new(testStr); | |
/// | |
/// while (iniParser.TryReadLine( | |
/// out int lineNumber, | |
/// out ReadOnlySpan<char> section, | |
/// out ReadOnlySpan<char> key, | |
/// out ReadOnlySpan<char> value | |
/// )) { | |
/// Console.WriteLine($"Line={lineNumber}, Section={section}, key={key}, value={value}"); | |
/// } | |
/// </code> | |
/// </example> | |
public ref struct IniParser { | |
private LineReader _lineReader; | |
private ReadOnlySpan<char> _lastSection; | |
/// <summary> | |
/// Constructor | |
/// </summary> | |
/// <param name="source">Source char span.</param> | |
/// <remarks>Return values of TryReadLine() return sub span of source.</remarks> | |
public IniParser(ReadOnlySpan<char> source) { | |
_lineReader = new LineReader(source); | |
_lastSection = ReadOnlySpan<char>.Empty; | |
} | |
/// <summary> | |
/// Read a line | |
/// </summary> | |
/// <param name="resultLineNumber">Line number. < 0 if failed</param> | |
/// <param name="section">Section name. Empty before the first section.</param> | |
/// <param name="key">Key name. Empty if there's no key.</param> | |
/// <param name="value">Value. Empty if there's no value.</param> | |
/// <returns>true if successfully read a line. false if read pointer reaches EOF.</returns> | |
/// <remarks>This function ignores empty line and comment line.</remarks> | |
public bool TryReadLine( | |
out int resultLineNumber, | |
out ReadOnlySpan<char> section, | |
out ReadOnlySpan<char> key, | |
out ReadOnlySpan<char> value | |
) { | |
while (true) { | |
int lineNumber = 0; | |
ReadOnlySpan<char> line0 = ReadOnlySpan<char>.Empty; | |
bool lineRead = _lineReader.TryReadLine(ref lineNumber, ref line0); | |
// EOF | |
if (! lineRead) { | |
resultLineNumber = -1; | |
section = _lastSection; | |
key = ReadOnlySpan<char>.Empty; | |
value = ReadOnlySpan<char>.Empty; | |
return false; | |
} | |
ReadOnlySpan<char> line = Chomp(line0); | |
// Skip blank line | |
if (line.Length == 0) { | |
continue; | |
} | |
// Skip ";" Comment | |
if (CheckFront(line, ';')) { | |
continue; | |
} | |
// Return : "[...]" Section | |
if (CheckFront(line, '[') && CheckBack(line, ']')) { | |
_lastSection = Chomp(line.Slice(1, line.Length - 2)); | |
resultLineNumber = lineNumber; | |
section = _lastSection; | |
key = ReadOnlySpan<char>.Empty; | |
value = ReadOnlySpan<char>.Empty; | |
return true; | |
} | |
// Return : Section, Key | |
if (! FindFirst(line, '=', out int index)) { | |
resultLineNumber = lineNumber; | |
section = _lastSection; | |
key = Chomp(line); | |
value = ReadOnlySpan<char>.Empty; | |
return true; | |
} | |
// Return : Section, Key, Value | |
resultLineNumber = lineNumber; | |
section = _lastSection; | |
key = Chomp(line.Slice(0, index)); | |
value = Chomp(line.Slice(index + 1, line.Length - index - 1)); | |
return true; | |
} | |
} | |
private static bool IsBlank(char c) => c == 0x20 || c == 0x09; | |
private static ReadOnlySpan<char> Chomp(ReadOnlySpan<char> line) { | |
int first = 0; | |
while (first < line.Length && IsBlank(line[first])) { | |
++first; | |
} | |
int last = line.Length - 1; | |
while (last > 0 && IsBlank(line[last])) { | |
--last; | |
} | |
int lineLength = last - first + 1; | |
return lineLength <= 0 | |
? ReadOnlySpan<char>.Empty | |
: line.Slice(first, lineLength); | |
} | |
private static bool CheckFront(ReadOnlySpan<char> line, char c) => line.Length > 0 && line[0] == c; | |
private static bool CheckBack(ReadOnlySpan<char> line, char c) => line.Length > 0 && line[^1] == c; | |
private static bool FindFirst(ReadOnlySpan<char> line, char c, out int index) { | |
for (int i = 0; i < line.Length; ++i) { | |
if (line[i] == c) { | |
index = i; | |
return true; | |
} | |
} | |
index = -1; | |
return false; | |
} | |
private ref struct LineReader { | |
private readonly ReadOnlySpan<char> _bytes; | |
private int _pos; | |
private int _lineNumber; | |
private const char NulChar = (char)0; | |
public LineReader(ReadOnlySpan<char> source) { | |
_bytes = source; | |
_pos = 0; | |
_lineNumber = 0; | |
} | |
public bool TryReadLine(ref int lineNumber, ref ReadOnlySpan<char> line) { | |
if (Eof()) { | |
return false; | |
} | |
_lineNumber += 1; | |
int lineStart = GetOffset(); | |
int lineLength; | |
while (true) { | |
char ch = PeekChar(0); | |
char nextChar = PeekChar(1); | |
bool lf = ch == 0x0a; | |
bool crLf = ch == 0x0d && nextChar == 0x0a; | |
bool eof = ch == NulChar; | |
bool eol = lf || crLf || eof; | |
if (! eol) { | |
SkipChar(); | |
continue; | |
} | |
// LF or CRLF or EOF | |
lineLength = GetOffset() - lineStart; | |
SkipChar(); | |
// CRLF | |
if (crLf) { | |
SkipChar(); | |
} | |
break; | |
} | |
lineNumber = _lineNumber; | |
line = lineLength <= 0 | |
? ReadOnlySpan<char>.Empty | |
: Slice(lineStart, lineLength); | |
return true; | |
} | |
private int GetOffset() => _pos; | |
private bool ValidOffset(int offset) => offset >= 0 && offset < _bytes.Length; | |
private bool Eof() => ! ValidOffset(GetOffset()); | |
private void SkipChar() { | |
if (! Eof()) { | |
_pos += 1; | |
} | |
} | |
private char PeekChar(int offset) { | |
int o = _pos + offset; | |
return ValidOffset(o) ? _bytes[o] : NulChar; | |
} | |
private ReadOnlySpan<char> Slice(int offset, int length) => | |
_bytes.Slice(offset, length); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment