Skip to content

Commit

Permalink
first version (Qiita Advent Calendar 2017)
Browse files Browse the repository at this point in the history
  • Loading branch information
wipiano committed Dec 10, 2017
1 parent c723714 commit ae03166
Show file tree
Hide file tree
Showing 12 changed files with 1,169 additions and 0 deletions.
726 changes: 726 additions & 0 deletions src/ParserCombinator.Tests/ChainingAssertion.cs

Large diffs are not rendered by default.

68 changes: 68 additions & 0 deletions src/ParserCombinator.Tests/CharParserTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
using System;
using Xunit;
using Xunit.Abstractions;
using static Xunit.Assert;
using static Xunit.AssertEx;

using static ParserCombinator.CharParsers;

namespace ParserCombinator.Tests
{
public class CharParserTest
{
[Fact]
public void AnyTest()
{
// Any はつねに成功
var result = Any(Source.Create("a")); // { IsSuccess: true, Result: 'a' }
result.IsSuccess.IsTrue();
result.Result.Is('a');
}

[Fact]
public void DigitTest()
{
// 数字だったら成功
var success = Digit(Source.Create("12a")); // { IsSuccess: true, Result: '1' }
success.IsSuccess.IsTrue();
success.Result.Is('1');

// 数字でなければ失敗
var failed = Digit(Source.Create("a12")); // { IsSuccess: false, Result: Exception }
failed.IsSuccess.IsFalse();
Throws(typeof(Exception), AccessToFailedResult(failed));
}

[Fact]
public void LiteralTest()
{
var parser = Literal('a');
var success = parser(Source.Create("abc")); // { IsSuccess: true, Result: 'a' }
success.IsSuccess.IsTrue();
success.Result.Is('a');
var failed = parser(Source.Create("ccc")); // { IsSuccess: false, Result: Exception }
failed.IsSuccess.IsFalse();
Throws(typeof(Exception), AccessToFailedResult(failed));
}


[Fact]
public void IsTest()
{
var lowerParser = Is(char.IsLower); // 小文字だけ受け付けるパーサ
var success = lowerParser(Source.Create("abc")); // { IsSuccess: true, Result: 'a' }
var failed = lowerParser(Source.Create("ABC")); // { IsSuccess: false, Result: Exception }

success.IsSuccess.IsTrue();
success.Result.Is('a');

failed.IsSuccess.IsFalse();
Throws(typeof(Exception), AccessToFailedResult(failed));
}

private static Action AccessToFailedResult<T>(ParseResult<T> result) => () =>
{
var tmp = result.Result;
};
}
}
14 changes: 14 additions & 0 deletions src/ParserCombinator.Tests/ParserCombinator.Tests.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp2.0</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.3.0-preview-20170628-02" />
<PackageReference Include="xunit" Version="2.2.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.2.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ParserCombinator\ParserCombinator.csproj" />
</ItemGroup>
</Project>
75 changes: 75 additions & 0 deletions src/ParserCombinator.Tests/PostalCodeParser.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
using System;
using System.Linq;
using Xunit;
using static Xunit.Assert;
using static ParserCombinator.ParseResultHelper;
using static ParserCombinator.CharParsers;

namespace ParserCombinator.Tests
{
public class PostalCodeParserTests
{
[Fact]
public void SimplePostalCodeParserTest()
{
// xxx-yyyy の xxx 部分
Parser<int> leftPart = Digit.Repeat(3).Map(chars => int.Parse(new string(chars.ToArray())));

// xxx-yyyy の yyyy 部分
Parser<int> rightPart = Digit.Repeat(4).Map(chars => int.Parse(new string(chars.ToArray())));

// xxx-yyyy の形式の郵便番号のパーサ
Parser<PostalCode> postalCodeParser = leftPart
.Left(Literal('-'))
.Sequence(rightPart, (left, right) => new PostalCode(left, right));

ParseResult<PostalCode> result = postalCodeParser(Source.Create("123-4567"));
PostalCode postalCode = result.Result;

result.IsSuccess.IsTrue();
postalCode.IsStructuralEqual(new PostalCode(123, 4567));
}

[Fact]
public void PostalCodeParserTest()
{
// xxx-yyyy の xxx 部分
Parser<int> leftPart = Digit.Repeat(3).Map(chars => int.Parse(new string(chars.ToArray())));

// xxx-yyyy の yyyy 部分
Parser<int> rightPart = Digit.Repeat(4).Map(chars => int.Parse(new string(chars.ToArray())));

// 普通の xxx-yyyy
Parser<PostalCode> normal = leftPart.Left(Literal('-')).Sequence(rightPart, (l, r) => new PostalCode(l, r));

// xxxyyyy
Parser<PostalCode> withoutSeparator = leftPart.Sequence(rightPart, (l, r) => new PostalCode(l, r));

Parser<PostalCode> postalCode = normal.Or(withoutSeparator);

// 〒 が付加されてもよい
Parser<PostalCode> postalCodeParser = Literal('〒').Right(postalCode).Or(postalCode);

var expected = new PostalCode(123, 4567);
postalCodeParser(Source.Create("123-4567")).Result.IsStructuralEqual(expected);
postalCodeParser(Source.Create("1234567")).Result.IsStructuralEqual(expected);
postalCodeParser(Source.Create("〒123-4567")).Result.IsStructuralEqual(expected);
postalCodeParser(Source.Create("〒1234567")).Result.IsStructuralEqual(expected);
}
}

public class PostalCode
{
public int LeftPart { get; }

public int RightPart { get; }

public PostalCode(int left, int right)
{
this.LeftPart = left;
this.RightPart = right;
}

public override string ToString() => $"{LeftPart}-{RightPart}";
}
}
28 changes: 28 additions & 0 deletions src/ParserCombinator.sln
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 2013
VisualStudioVersion = 12.0.0.0
MinimumVisualStudioVersion = 10.0.0.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ParserCombinator", "ParserCombinator/ParserCombinator.csproj", "{BD5A64D0-B4CE-4130-A43B-26519E0B9665}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ParserCombinator.Tests", "ParserCombinator.Tests\ParserCombinator.Tests.csproj", "{1593751F-40C6-44DA-8D0C-B3588A0DBFEE}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{BD5A64D0-B4CE-4130-A43B-26519E0B9665}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BD5A64D0-B4CE-4130-A43B-26519E0B9665}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BD5A64D0-B4CE-4130-A43B-26519E0B9665}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BD5A64D0-B4CE-4130-A43B-26519E0B9665}.Release|Any CPU.Build.0 = Release|Any CPU
{1593751F-40C6-44DA-8D0C-B3588A0DBFEE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1593751F-40C6-44DA-8D0C-B3588A0DBFEE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1593751F-40C6-44DA-8D0C-B3588A0DBFEE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1593751F-40C6-44DA-8D0C-B3588A0DBFEE}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
EndGlobal
33 changes: 33 additions & 0 deletions src/ParserCombinator/CharParsers.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using System;

namespace ParserCombinator
{
using static ParseResultHelper;

public static class CharParsers
{
public static Parser<char> Any { get; } = (Source s) =>
{
var (c, next) = s.Read();
return Success(next, c);
};

public static Parser<char> Digit { get; } = (Source s) =>
{
var (c, next) = s.Read();
return char.IsDigit(c) ? Success(next, c) : Failed<char>(next, "Is not a digit.");
};

public static Parser<char> Literal(char literal) => (Source s) =>
{
var (c, next) = s.Read();
return c == literal ? Success(next, c) : Failed<char>(next, $"{c} is not equals {literal}");
};

public static Parser<char> Is(Func<char, bool> predicate) => (Source s) =>
{
var (c, next) = s.Read();
return predicate(c) ? Success(next, c) : Failed<char>(next, $"predicate({c}) returns false.");
};
}
}
92 changes: 92 additions & 0 deletions src/ParserCombinator/Combinators.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
using System;
using System.Collections.Immutable;
using System.Net.Mime;
using static ParserCombinator.ParseResultHelper;

namespace ParserCombinator
{
public static class Combinators
{
public static Parser<ImmutableList<T>> Many<T>(this Parser<T> parser)
{
ParseResult<ImmutableList<T>> Impl(Source s, ImmutableList<T> results)
{
var result = parser(s);

return result.IsSuccess
? Impl(result.Source, results.Add(result.Result))
: Success(s, results);
}

return (Source s) => Impl(s, ImmutableList<T>.Empty);
}

public static Parser<ImmutableList<T>> Repeat<T>(this Parser<T> parser, int count)
{
ParseResult<ImmutableList<T>> Impl(Source s, int c, ImmutableList<T> results)
{
if (c == 0)
{
// 0 回を指定されたら終わり
return Success(s, results);
}

var result = parser(s);

return result.IsSuccess
? Impl(result.Source, c - 1, results.Add(result.Result))
: Failed<ImmutableList<T>>(result.Source, result.Reason);
}

return (Source s) => Impl(s, count, ImmutableList<T>.Empty);
}

public static Parser<ImmutableList<T>> Sequence<T>(this Parser<T> first, Parser<T> second) =>
first.Sequence(second, (f, s) => ImmutableList<T>.Empty.Add(f).Add(s));

public static Parser<ImmutableList<T>> Sequence<T>(this Parser<ImmutableList<T>> first, Parser<T> second) =>
first.Sequence(second, (f, s) => f.Add(s));

public static Parser<TResult> Sequence<TFirst, TSecond, TResult>(this Parser<TFirst> first, Parser<TSecond> second, Func<TFirst, TSecond, TResult> resultSelector) =>
(Source s) =>
{
var firstResult = first(s);
if (firstResult.IsSuccess)
{
var secondResult = second(firstResult.Source);
return secondResult.IsSuccess
? Success(secondResult.Source, resultSelector(firstResult.Result, secondResult.Result))
: Failed<TResult>(secondResult.Source, secondResult.Reason);
}
else
{
return Failed<TResult>(firstResult.Source, firstResult.Reason);
}
};

public static Parser<T> Or<T>(this Parser<T> left, Parser<T> right) => (Source s) =>
{
var leftResult = left(s);
return leftResult.IsSuccess
? leftResult
: right(s);
};

public static Parser<TLeft> Left<TLeft, TRight>(this Parser<TLeft> left, Parser<TRight> right) =>
left.Sequence(right, (l, r) => l);

public static Parser<TRight> Right<TLeft, TRight>(this Parser<TLeft> left, Parser<TRight> right) =>
left.Sequence(right, (l, r) => r);

public static Parser<TResult> Map<TParser, TResult>(this Parser<TParser> parser, Func<TParser, TResult> mapper) =>
(Source s) =>
{
var result = parser(s);
return result.IsSuccess
? Success(result.Source, mapper(result.Result))
: Failed<TResult>(result.Source, result.Reason);
};
}
}
30 changes: 30 additions & 0 deletions src/ParserCombinator/ParseResult.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using System;

namespace ParserCombinator
{
public struct ParseResult<T>
{
/// <summary>実行後の Source</summary>
public Source Source { get; }

/// <summary>成功したかどうか</summary>
public bool IsSuccess { get; }

/// <summary>パース結果</summary>
public T Result =>
this.IsSuccess ? _result : throw new Exception($"Parse error: {Reason}");

private readonly T _result;

// 失敗した理由
public string Reason { get; }

internal ParseResult(Source source, bool isSuccess, T result, string reason)
{
this.Source = source;
this.IsSuccess = isSuccess;
_result = result;
this.Reason = reason;
}
}
}
11 changes: 11 additions & 0 deletions src/ParserCombinator/ParseResultHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace ParserCombinator
{
public static class ParseResultHelper
{
public static ParseResult<T> Success<T>(Source source, T result)
=> new ParseResult<T>(source, true, result, default);

public static ParseResult<T> Failed<T>(Source source, string reason)
=> new ParseResult<T>(source, false, default, reason);
}
}
4 changes: 4 additions & 0 deletions src/ParserCombinator/Parser.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
namespace ParserCombinator
{
public delegate ParseResult<T> Parser<T>(Source source);
}
9 changes: 9 additions & 0 deletions src/ParserCombinator/ParserCombinator.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>latest</LangVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.Collections.Immutable" Version="1.4.0" />
</ItemGroup>
</Project>

0 comments on commit ae03166

Please sign in to comment.