晴耕雨読

working in the fields on fine days and reading books on rainy days

[C#] 構文解析器Spracheで可変長引数を解析する (DelimitedBy)

構文解析器のSpracheでカンマ区切りなどで表される可変長引数を読み取るメソッド一覧について説明します。

DelimitedBy()

第2引数のParserを区切り文字として、第1引数のParserで読み取ります。

  • Parser<IEnumerable<T>> DelimitedBy<T, U>(this Parser<T> parser, Parser<U> delimiter)
Parser<string> typeReference = Parse.Identifier(Parse.Letter, Parse.LetterOrDigit);

Parser<IEnumerable<string>> typeParameters =
    from open in Parse.Char('<')
    from elements in typeReference.DelimitedBy(Parse.Char(',').Token())
    from close in Parse.Char('>')
    select elements;

Assert.Equal(["string"], typeParameters.Parse("<string>"));
Assert.Equal(["string", "int"], typeParameters.Parse("<string, int>"));

// unexpected ','; expected >
Assert.Throws<ParseException>(() => typeParameters.Parse("<string,>"));

// unexpected '>'; expected letter
Assert.Throws<ParseException>(() => typeParameters.Parse("<>"));

上記は C# におけるジェネリックの表現を Sprache で解析して型の一覧をリストにする方法の一例です。

DelimitedBy を使うときの注意点として、末尾の区切り文字は読み取りを行いません。 そのため、末尾に区切り文字が入力される可能性があるときは、Optional() を使って末尾の区切り文字を許容した読み取りにする必要があります。

Parser<IEnumerable<string>> array =
    from open in Parse.Char('[')
    from elements in Parse.Number.DelimitedBy(Parse.Char(',').Token()).Optional()
    from trailing in Parse.Char(',').Token().Optional()
    from close in Parse.Char(']')
    select elements.GetOrElse([]);

Assert.Equal(["1", "2", "3"], array.Parse("[1, 2, 3]"));
Assert.Equal(["1", "2"], array.Parse("[1, 2, ]"));
Assert.Equal([], array.Parse("[]"));

XDelimitedBy()

基本的に DelimitedBy と同じですが、Parserが1文字以上マッチして全体にはマッチしないときに、ParseExceptionエラーを返すようになります。

  • Parser<IEnumerable<T>> XDelimitedBy<T, U>(this Parser<T> itemParser, Parser<U> delimiter)
Parser<IEnumerable<string>> numbers = Parse.Number.DelimitedBy(Parse.Char(',').Token());
Parser<IEnumerable<string>> numbersX = Parse.Number.XDelimitedBy(Parse.Char(',').Token());

Assert.Equal(["1", "2"], numbers.Parse("1, 2, "));
Assert.Throws<ParseException>(() => numbersX.Parse("1, 2, "));

Assert.Equal(["1", "2"], numbers.Parse("1, 2a, 3"));
Assert.Equal(["1", "2"], numbersX.Parse("1, 2a, 3"));

Assert.Equal(["1", "2"], numbers.Parse("1, 2 "));
Assert.Throws<ParseException>(() => numbersX.Parse("1, 2 "));  // 区切り文字の前後空白にマッチするためエラーする

以上です。

参考資料