Skip to content

Commit ae03166

Browse files
committed
first version (Qiita Advent Calendar 2017)
1 parent c723714 commit ae03166

12 files changed

+1169
-0
lines changed

src/ParserCombinator.Tests/ChainingAssertion.cs

Lines changed: 726 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
using System;
2+
using Xunit;
3+
using Xunit.Abstractions;
4+
using static Xunit.Assert;
5+
using static Xunit.AssertEx;
6+
7+
using static ParserCombinator.CharParsers;
8+
9+
namespace ParserCombinator.Tests
10+
{
11+
public class CharParserTest
12+
{
13+
[Fact]
14+
public void AnyTest()
15+
{
16+
// Any はつねに成功
17+
var result = Any(Source.Create("a")); // { IsSuccess: true, Result: 'a' }
18+
result.IsSuccess.IsTrue();
19+
result.Result.Is('a');
20+
}
21+
22+
[Fact]
23+
public void DigitTest()
24+
{
25+
// 数字だったら成功
26+
var success = Digit(Source.Create("12a")); // { IsSuccess: true, Result: '1' }
27+
success.IsSuccess.IsTrue();
28+
success.Result.Is('1');
29+
30+
// 数字でなければ失敗
31+
var failed = Digit(Source.Create("a12")); // { IsSuccess: false, Result: Exception }
32+
failed.IsSuccess.IsFalse();
33+
Throws(typeof(Exception), AccessToFailedResult(failed));
34+
}
35+
36+
[Fact]
37+
public void LiteralTest()
38+
{
39+
var parser = Literal('a');
40+
var success = parser(Source.Create("abc")); // { IsSuccess: true, Result: 'a' }
41+
success.IsSuccess.IsTrue();
42+
success.Result.Is('a');
43+
var failed = parser(Source.Create("ccc")); // { IsSuccess: false, Result: Exception }
44+
failed.IsSuccess.IsFalse();
45+
Throws(typeof(Exception), AccessToFailedResult(failed));
46+
}
47+
48+
49+
[Fact]
50+
public void IsTest()
51+
{
52+
var lowerParser = Is(char.IsLower); // 小文字だけ受け付けるパーサ
53+
var success = lowerParser(Source.Create("abc")); // { IsSuccess: true, Result: 'a' }
54+
var failed = lowerParser(Source.Create("ABC")); // { IsSuccess: false, Result: Exception }
55+
56+
success.IsSuccess.IsTrue();
57+
success.Result.Is('a');
58+
59+
failed.IsSuccess.IsFalse();
60+
Throws(typeof(Exception), AccessToFailedResult(failed));
61+
}
62+
63+
private static Action AccessToFailedResult<T>(ParseResult<T> result) => () =>
64+
{
65+
var tmp = result.Result;
66+
};
67+
}
68+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
<PropertyGroup>
3+
<TargetFramework>netcoreapp2.0</TargetFramework>
4+
<IsPackable>false</IsPackable>
5+
</PropertyGroup>
6+
<ItemGroup>
7+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.3.0-preview-20170628-02" />
8+
<PackageReference Include="xunit" Version="2.2.0" />
9+
<PackageReference Include="xunit.runner.visualstudio" Version="2.2.0" />
10+
</ItemGroup>
11+
<ItemGroup>
12+
<ProjectReference Include="..\ParserCombinator\ParserCombinator.csproj" />
13+
</ItemGroup>
14+
</Project>
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
using System;
2+
using System.Linq;
3+
using Xunit;
4+
using static Xunit.Assert;
5+
using static ParserCombinator.ParseResultHelper;
6+
using static ParserCombinator.CharParsers;
7+
8+
namespace ParserCombinator.Tests
9+
{
10+
public class PostalCodeParserTests
11+
{
12+
[Fact]
13+
public void SimplePostalCodeParserTest()
14+
{
15+
// xxx-yyyy の xxx 部分
16+
Parser<int> leftPart = Digit.Repeat(3).Map(chars => int.Parse(new string(chars.ToArray())));
17+
18+
// xxx-yyyy の yyyy 部分
19+
Parser<int> rightPart = Digit.Repeat(4).Map(chars => int.Parse(new string(chars.ToArray())));
20+
21+
// xxx-yyyy の形式の郵便番号のパーサ
22+
Parser<PostalCode> postalCodeParser = leftPart
23+
.Left(Literal('-'))
24+
.Sequence(rightPart, (left, right) => new PostalCode(left, right));
25+
26+
ParseResult<PostalCode> result = postalCodeParser(Source.Create("123-4567"));
27+
PostalCode postalCode = result.Result;
28+
29+
result.IsSuccess.IsTrue();
30+
postalCode.IsStructuralEqual(new PostalCode(123, 4567));
31+
}
32+
33+
[Fact]
34+
public void PostalCodeParserTest()
35+
{
36+
// xxx-yyyy の xxx 部分
37+
Parser<int> leftPart = Digit.Repeat(3).Map(chars => int.Parse(new string(chars.ToArray())));
38+
39+
// xxx-yyyy の yyyy 部分
40+
Parser<int> rightPart = Digit.Repeat(4).Map(chars => int.Parse(new string(chars.ToArray())));
41+
42+
// 普通の xxx-yyyy
43+
Parser<PostalCode> normal = leftPart.Left(Literal('-')).Sequence(rightPart, (l, r) => new PostalCode(l, r));
44+
45+
// xxxyyyy
46+
Parser<PostalCode> withoutSeparator = leftPart.Sequence(rightPart, (l, r) => new PostalCode(l, r));
47+
48+
Parser<PostalCode> postalCode = normal.Or(withoutSeparator);
49+
50+
// 〒 が付加されてもよい
51+
Parser<PostalCode> postalCodeParser = Literal('〒').Right(postalCode).Or(postalCode);
52+
53+
var expected = new PostalCode(123, 4567);
54+
postalCodeParser(Source.Create("123-4567")).Result.IsStructuralEqual(expected);
55+
postalCodeParser(Source.Create("1234567")).Result.IsStructuralEqual(expected);
56+
postalCodeParser(Source.Create("〒123-4567")).Result.IsStructuralEqual(expected);
57+
postalCodeParser(Source.Create("〒1234567")).Result.IsStructuralEqual(expected);
58+
}
59+
}
60+
61+
public class PostalCode
62+
{
63+
public int LeftPart { get; }
64+
65+
public int RightPart { get; }
66+
67+
public PostalCode(int left, int right)
68+
{
69+
this.LeftPart = left;
70+
this.RightPart = right;
71+
}
72+
73+
public override string ToString() => $"{LeftPart}-{RightPart}";
74+
}
75+
}

src/ParserCombinator.sln

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
2+
Microsoft Visual Studio Solution File, Format Version 12.00
3+
# Visual Studio 2013
4+
VisualStudioVersion = 12.0.0.0
5+
MinimumVisualStudioVersion = 10.0.0.1
6+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ParserCombinator", "ParserCombinator/ParserCombinator.csproj", "{BD5A64D0-B4CE-4130-A43B-26519E0B9665}"
7+
EndProject
8+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ParserCombinator.Tests", "ParserCombinator.Tests\ParserCombinator.Tests.csproj", "{1593751F-40C6-44DA-8D0C-B3588A0DBFEE}"
9+
EndProject
10+
Global
11+
GlobalSection(SolutionConfigurationPlatforms) = preSolution
12+
Debug|Any CPU = Debug|Any CPU
13+
Release|Any CPU = Release|Any CPU
14+
EndGlobalSection
15+
GlobalSection(ProjectConfigurationPlatforms) = postSolution
16+
{BD5A64D0-B4CE-4130-A43B-26519E0B9665}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
17+
{BD5A64D0-B4CE-4130-A43B-26519E0B9665}.Debug|Any CPU.Build.0 = Debug|Any CPU
18+
{BD5A64D0-B4CE-4130-A43B-26519E0B9665}.Release|Any CPU.ActiveCfg = Release|Any CPU
19+
{BD5A64D0-B4CE-4130-A43B-26519E0B9665}.Release|Any CPU.Build.0 = Release|Any CPU
20+
{1593751F-40C6-44DA-8D0C-B3588A0DBFEE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
21+
{1593751F-40C6-44DA-8D0C-B3588A0DBFEE}.Debug|Any CPU.Build.0 = Debug|Any CPU
22+
{1593751F-40C6-44DA-8D0C-B3588A0DBFEE}.Release|Any CPU.ActiveCfg = Release|Any CPU
23+
{1593751F-40C6-44DA-8D0C-B3588A0DBFEE}.Release|Any CPU.Build.0 = Release|Any CPU
24+
EndGlobalSection
25+
GlobalSection(SolutionProperties) = preSolution
26+
HideSolutionNode = FALSE
27+
EndGlobalSection
28+
EndGlobal

src/ParserCombinator/CharParsers.cs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
using System;
2+
3+
namespace ParserCombinator
4+
{
5+
using static ParseResultHelper;
6+
7+
public static class CharParsers
8+
{
9+
public static Parser<char> Any { get; } = (Source s) =>
10+
{
11+
var (c, next) = s.Read();
12+
return Success(next, c);
13+
};
14+
15+
public static Parser<char> Digit { get; } = (Source s) =>
16+
{
17+
var (c, next) = s.Read();
18+
return char.IsDigit(c) ? Success(next, c) : Failed<char>(next, "Is not a digit.");
19+
};
20+
21+
public static Parser<char> Literal(char literal) => (Source s) =>
22+
{
23+
var (c, next) = s.Read();
24+
return c == literal ? Success(next, c) : Failed<char>(next, $"{c} is not equals {literal}");
25+
};
26+
27+
public static Parser<char> Is(Func<char, bool> predicate) => (Source s) =>
28+
{
29+
var (c, next) = s.Read();
30+
return predicate(c) ? Success(next, c) : Failed<char>(next, $"predicate({c}) returns false.");
31+
};
32+
}
33+
}

src/ParserCombinator/Combinators.cs

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
using System;
2+
using System.Collections.Immutable;
3+
using System.Net.Mime;
4+
using static ParserCombinator.ParseResultHelper;
5+
6+
namespace ParserCombinator
7+
{
8+
public static class Combinators
9+
{
10+
public static Parser<ImmutableList<T>> Many<T>(this Parser<T> parser)
11+
{
12+
ParseResult<ImmutableList<T>> Impl(Source s, ImmutableList<T> results)
13+
{
14+
var result = parser(s);
15+
16+
return result.IsSuccess
17+
? Impl(result.Source, results.Add(result.Result))
18+
: Success(s, results);
19+
}
20+
21+
return (Source s) => Impl(s, ImmutableList<T>.Empty);
22+
}
23+
24+
public static Parser<ImmutableList<T>> Repeat<T>(this Parser<T> parser, int count)
25+
{
26+
ParseResult<ImmutableList<T>> Impl(Source s, int c, ImmutableList<T> results)
27+
{
28+
if (c == 0)
29+
{
30+
// 0 回を指定されたら終わり
31+
return Success(s, results);
32+
}
33+
34+
var result = parser(s);
35+
36+
return result.IsSuccess
37+
? Impl(result.Source, c - 1, results.Add(result.Result))
38+
: Failed<ImmutableList<T>>(result.Source, result.Reason);
39+
}
40+
41+
return (Source s) => Impl(s, count, ImmutableList<T>.Empty);
42+
}
43+
44+
public static Parser<ImmutableList<T>> Sequence<T>(this Parser<T> first, Parser<T> second) =>
45+
first.Sequence(second, (f, s) => ImmutableList<T>.Empty.Add(f).Add(s));
46+
47+
public static Parser<ImmutableList<T>> Sequence<T>(this Parser<ImmutableList<T>> first, Parser<T> second) =>
48+
first.Sequence(second, (f, s) => f.Add(s));
49+
50+
public static Parser<TResult> Sequence<TFirst, TSecond, TResult>(this Parser<TFirst> first, Parser<TSecond> second, Func<TFirst, TSecond, TResult> resultSelector) =>
51+
(Source s) =>
52+
{
53+
var firstResult = first(s);
54+
if (firstResult.IsSuccess)
55+
{
56+
var secondResult = second(firstResult.Source);
57+
58+
return secondResult.IsSuccess
59+
? Success(secondResult.Source, resultSelector(firstResult.Result, secondResult.Result))
60+
: Failed<TResult>(secondResult.Source, secondResult.Reason);
61+
}
62+
else
63+
{
64+
return Failed<TResult>(firstResult.Source, firstResult.Reason);
65+
}
66+
};
67+
68+
public static Parser<T> Or<T>(this Parser<T> left, Parser<T> right) => (Source s) =>
69+
{
70+
var leftResult = left(s);
71+
72+
return leftResult.IsSuccess
73+
? leftResult
74+
: right(s);
75+
};
76+
77+
public static Parser<TLeft> Left<TLeft, TRight>(this Parser<TLeft> left, Parser<TRight> right) =>
78+
left.Sequence(right, (l, r) => l);
79+
80+
public static Parser<TRight> Right<TLeft, TRight>(this Parser<TLeft> left, Parser<TRight> right) =>
81+
left.Sequence(right, (l, r) => r);
82+
83+
public static Parser<TResult> Map<TParser, TResult>(this Parser<TParser> parser, Func<TParser, TResult> mapper) =>
84+
(Source s) =>
85+
{
86+
var result = parser(s);
87+
return result.IsSuccess
88+
? Success(result.Source, mapper(result.Result))
89+
: Failed<TResult>(result.Source, result.Reason);
90+
};
91+
}
92+
}

src/ParserCombinator/ParseResult.cs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
using System;
2+
3+
namespace ParserCombinator
4+
{
5+
public struct ParseResult<T>
6+
{
7+
/// <summary>実行後の Source</summary>
8+
public Source Source { get; }
9+
10+
/// <summary>成功したかどうか</summary>
11+
public bool IsSuccess { get; }
12+
13+
/// <summary>パース結果</summary>
14+
public T Result =>
15+
this.IsSuccess ? _result : throw new Exception($"Parse error: {Reason}");
16+
17+
private readonly T _result;
18+
19+
// 失敗した理由
20+
public string Reason { get; }
21+
22+
internal ParseResult(Source source, bool isSuccess, T result, string reason)
23+
{
24+
this.Source = source;
25+
this.IsSuccess = isSuccess;
26+
_result = result;
27+
this.Reason = reason;
28+
}
29+
}
30+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
namespace ParserCombinator
2+
{
3+
public static class ParseResultHelper
4+
{
5+
public static ParseResult<T> Success<T>(Source source, T result)
6+
=> new ParseResult<T>(source, true, result, default);
7+
8+
public static ParseResult<T> Failed<T>(Source source, string reason)
9+
=> new ParseResult<T>(source, false, default, reason);
10+
}
11+
}

src/ParserCombinator/Parser.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
namespace ParserCombinator
2+
{
3+
public delegate ParseResult<T> Parser<T>(Source source);
4+
}

0 commit comments

Comments
 (0)