Skip to content

Instantly share code, notes, and snippets.

@t-mat
Last active January 4, 2024 07:39
Show Gist options
  • Save t-mat/b026bd24274c13bf09cff1c623a2e4aa to your computer and use it in GitHub Desktop.
Save t-mat/b026bd24274c13bf09cff1c623a2e4aa to your computer and use it in GitHub Desktop.
[C#]Simple ReadOnlySpan<char> .ini parser
// 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&lt;char&gt; 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&lt;char&gt; section,
/// out ReadOnlySpan&lt;char&gt; key,
/// out ReadOnlySpan&lt;char&gt; 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. &lt; 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