From ae0316691bbee5086be9468d1a4ab6ba19e7ab36 Mon Sep 17 00:00:00 2001 From: Kohei Hakoishi Date: Sun, 10 Dec 2017 15:54:14 +0900 Subject: [PATCH] first version (Qiita Advent Calendar 2017) --- .../ChainingAssertion.cs | 726 ++++++++++++++++++ src/ParserCombinator.Tests/CharParserTest.cs | 68 ++ .../ParserCombinator.Tests.csproj | 14 + .../PostalCodeParser.cs | 75 ++ src/ParserCombinator.sln | 28 + src/ParserCombinator/CharParsers.cs | 33 + src/ParserCombinator/Combinators.cs | 92 +++ src/ParserCombinator/ParseResult.cs | 30 + src/ParserCombinator/ParseResultHelper.cs | 11 + src/ParserCombinator/Parser.cs | 4 + src/ParserCombinator/ParserCombinator.csproj | 9 + src/ParserCombinator/Source.cs | 79 ++ 12 files changed, 1169 insertions(+) create mode 100644 src/ParserCombinator.Tests/ChainingAssertion.cs create mode 100644 src/ParserCombinator.Tests/CharParserTest.cs create mode 100644 src/ParserCombinator.Tests/ParserCombinator.Tests.csproj create mode 100644 src/ParserCombinator.Tests/PostalCodeParser.cs create mode 100644 src/ParserCombinator.sln create mode 100644 src/ParserCombinator/CharParsers.cs create mode 100644 src/ParserCombinator/Combinators.cs create mode 100644 src/ParserCombinator/ParseResult.cs create mode 100644 src/ParserCombinator/ParseResultHelper.cs create mode 100644 src/ParserCombinator/Parser.cs create mode 100644 src/ParserCombinator/ParserCombinator.csproj create mode 100644 src/ParserCombinator/Source.cs diff --git a/src/ParserCombinator.Tests/ChainingAssertion.cs b/src/ParserCombinator.Tests/ChainingAssertion.cs new file mode 100644 index 0000000..ffd95a3 --- /dev/null +++ b/src/ParserCombinator.Tests/ChainingAssertion.cs @@ -0,0 +1,726 @@ +/*-------------------------------------------------------------------------- + * Chaining Assertion + * ver 1.7.1.0 (Apr. 29th, 2013) + * + * created and maintained by neuecc + * licensed under Microsoft Public License(Ms-PL) + * http://chainingassertion.codeplex.com/ + *--------------------------------------------------------------------------*/ + +/* -- Tutorial -- + * | at first, include this file on xUnit.net Project. + * + * | three example, "Is" overloads. + * + * // This same as Assert.Equal(25, Math.Pow(5, 2)) + * Math.Pow(5, 2).Is(25); + * + * // This same as Assert.True("foobar".StartsWith("foo") && "foobar".EndWith("bar")) + * "foobar".Is(s => s.StartsWith("foo") && s.EndsWith("bar")); + * + * // This same as Assert.Equal(Enumerable.Range(1,5).ToArray(), new[]{1, 2, 3, 4, 5}.ToArray()) + * // it is sequence value compare + * Enumerable.Range(1, 5).Is(1, 2, 3, 4, 5); + * + * | CollectionAssert + * | if you want to use CollectionAssert Methods then use Linq to Objects and Is + * + * var array = new[] { 1, 3, 7, 8 }; + * array.Count().Is(4); + * array.Contains(8).IsTrue(); // IsTrue() == Is(true) + * array.All(i => i < 5).IsFalse(); // IsFalse() == Is(false) + * array.Any().Is(true); + * new int[] { }.Any().Is(false); // IsEmpty + * array.OrderBy(x => x).Is(array); // IsOrdered + * + * | Other Assertions + * + * // Null Assertions + * Object obj = null; + * obj.IsNull(); // Assert.Null(obj) + * new Object().IsNotNull(); // Assert.NotNull(obj) + * + * // Not Assertion + * "foobar".IsNot("fooooooo"); // Assert.NotEqual + * new[] { "a", "z", "x" }.IsNot("a", "x", "z"); /// Assert.NotEqual + * + * // ReferenceEqual Assertion + * var tuple = Tuple.Create("foo"); + * tuple.IsSameReferenceAs(tuple); // Assert.Same + * tuple.IsNotSameReferenceAs(Tuple.Create("foo")); // Assert.NotSame + * + * // Type Assertion + * "foobar".IsInstanceOf(); // Assert.IsType + * (999).IsNotInstanceOf(); // Assert.IsNotType + * + * | Advanced Collection Assertion + * + * var lower = new[] { "a", "b", "c" }; + * var upper = new[] { "A", "B", "C" }; + * + * // Comparer CollectionAssert, use IEqualityComparer or Func delegate + * lower.Is(upper, StringComparer.InvariantCultureIgnoreCase); + * lower.Is(upper, (x, y) => x.ToUpper() == y.ToUpper()); + * + * // or you can use Linq to Objects - SequenceEqual + * lower.SequenceEqual(upper, StringComparer.InvariantCultureIgnoreCase).Is(true); + * + * | StructuralEqual + * + * class MyClass + * { + * public int IntProp { get; set; } + * public string StrField; + * } + * + * var mc1 = new MyClass() { IntProp = 10, StrField = "foo" }; + * var mc2 = new MyClass() { IntProp = 10, StrField = "foo" }; + * + * mc1.IsStructuralEqual(mc2); // deep recursive value equality compare + * + * mc1.IntProp = 20; + * mc1.IsNotStructuralEqual(mc2); + * + * | DynamicAccessor + * + * // AsDynamic convert to "dynamic" that can call private method/property/field/indexer. + * + * // a class and private field/property/method. + * public class PrivateMock + * { + * private string privateField = "homu"; + * + * private string PrivateProperty + * { + * get { return privateField + privateField; } + * set { privateField = value; } + * } + * + * private string PrivateMethod(int count) + * { + * return string.Join("", Enumerable.Repeat(privateField, count)); + * } + * } + * + * // call private property. + * var actual = new PrivateMock().AsDynamic().PrivateProperty; + * Assert.AreEqual("homuhomu", actual); + * + * // dynamic can't invoke extension methods. + * // if you want to invoke "Is" then cast type. + * (new PrivateMock().AsDynamic().PrivateMethod(3) as string).Is("homuhomuhomu"); + * + * // set value + * var mock = new PrivateMock().AsDynamic(); + * mock.PrivateProperty = "mogumogu"; + * (mock.privateField as string).Is("mogumogu"); + * + * -- more details see project home --*/ + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.Contracts; +using System.Dynamic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using Xunit.Sdk; + +namespace Xunit +{ + #region Extensions + + [System.Diagnostics.DebuggerStepThroughAttribute] + [ContractVerification(false)] + public static partial class AssertEx + { + /// Assert.Equal, if T is IEnumerable then compare value equality + public static void Is(this T actual, T expected) + { + if (typeof(T) != typeof(string) && typeof(IEnumerable).IsAssignableFrom(typeof(T))) + { + Assert.Equal( + ((IEnumerable)expected).Cast().ToArray(), + ((IEnumerable)actual).Cast().ToArray()); + return; + } + + Assert.Equal(expected, actual); + } + + /// Assert.True(predicate(value)) + public static void Is(this T value, Expression> predicate, string message = "") + { + var condition = predicate.Compile().Invoke(value); + + var paramName = predicate.Parameters.First().Name; + string msg = ""; + try + { + var dumper = new ExpressionDumper(value, predicate.Parameters.Single()); + dumper.Visit(predicate); + var dump = string.Join(", ", dumper.Members.Select(kvp => kvp.Key + " = " + kvp.Value)); + msg = string.Format("\r\n{0} = {1}\r\n{2}\r\n{3}{4}", + paramName, value, dump, predicate, + string.IsNullOrEmpty(message) ? "" : ", " + message); + } + catch + { + msg = string.Format("{0} = {1}, {2}{3}", + paramName, value, predicate, + string.IsNullOrEmpty(message) ? "" : ", " + message); + } + + Assert.True(condition, msg); + } + + /// Assert.Equal + public static void Is(this T actual, T expected, IEqualityComparer comparer) + { + Assert.Equal(expected, actual, comparer); + } + + /// Assert.Equal(sequence value compare) + public static void Is(this IEnumerable actual, params T[] expected) + { + Is(actual, expected.AsEnumerable()); + } + + /// Assert.Equal(sequence value compare) + public static void Is(this IEnumerable actual, IEnumerable expected) + { + Assert.Equal(expected.ToArray(), actual.ToArray()); + } + + /// Assert.True(actual.SequenceEqual(expected, comparer)) + public static void Is(this IEnumerable actual, IEnumerable expected, IEqualityComparer comparer) + { + Assert.True(actual.SequenceEqual(expected, comparer)); + } + + /// Assert.True(actual.SequenceEqual(expected, comparison)) + public static void Is(this IEnumerable actual, IEnumerable expected, Func equalityComparison) + { + Assert.True(actual.SequenceEqual(expected, new EqualityComparer(equalityComparison))); + } + + /// Assert.NotEqual, if T is IEnumerable then check value equality + public static void IsNot(this T actual, T notExpected) + { + if (typeof(T) != typeof(string) && typeof(IEnumerable).IsAssignableFrom(typeof(T))) + { + Assert.NotEqual( + ((IEnumerable)actual).Cast().ToArray(), + ((IEnumerable)notExpected).Cast().ToArray()); + return; + } + + Assert.NotEqual(notExpected, actual); + } + + /// Assert.NotEqual + public static void IsNot(this T actual, T notExpected, IEqualityComparer comparer) + { + Assert.NotEqual(notExpected, actual, comparer); + } + + /// Assert.NotEqual(sequence value compare) + public static void IsNot(this IEnumerable actual, params T[] notExpected) + { + IsNot(actual, notExpected.AsEnumerable()); + } + + /// Assert.NotEqual(sequence value compare) + public static void IsNot(this IEnumerable actual, IEnumerable notExpected) + { + Assert.NotEqual(notExpected.ToArray(), actual.ToArray()); + } + + /// Assert.False(actual.SequenceEqual(notExpected, comparer)) + public static void IsNot(this IEnumerable actual, IEnumerable notExpected, IEqualityComparer comparer) + { + Assert.False(actual.SequenceEqual(notExpected, comparer)); + } + + /// Assert.False(actual.SequenceEqual(notExpected, comparison)) + public static void IsNot(this IEnumerable actual, IEnumerable notExpected, Func equalityComparison) + { + Assert.False(actual.SequenceEqual(notExpected, new EqualityComparer(equalityComparison))); + } + + /// Assert.Null + public static void IsNull(this T value) + { + Assert.Null(value); + } + + /// Assert.NotNull + public static void IsNotNull(this T value) + { + Assert.NotNull(value); + } + + /// Is(true) + public static void IsTrue(this bool value) + { + value.Is(true); + } + + /// Is(false) + public static void IsFalse(this bool value) + { + value.Is(false); + } + + /// Assert.Same + public static void IsSameReferenceAs(this T actual, T expected) + { + Assert.Same(expected, actual); + } + + /// Assert.NotSame + public static void IsNotSameReferenceAs(this T actual, T notExpected) + { + Assert.NotSame(notExpected, actual); + } + + /// Assert.IsType + public static TExpected IsInstanceOf(this object value) + { + Assert.IsType(value); + return (TExpected)value; + } + + /// Assert.IsNotType + public static void IsNotInstanceOf(this object value) + { + Assert.IsNotType(value); + } + + /// EqualityComparison to IEqualityComparer Converter for CollectionAssert + private class EqualityComparer : IEqualityComparer + { + readonly Func comparison; + + public EqualityComparer(Func comparison) + { + this.comparison = comparison; + } + + + public bool Equals(T x, T y) + { + return (comparison != null) + ? comparison(x, y) + : object.Equals(x, y); + } + + public int GetHashCode(T obj) + { + return 0; + } + } + + private class ReflectAccessor + { + public Func GetValue { get; private set; } + public Action SetValue { get; private set; } + + public ReflectAccessor(T target, string name) + { + var field = typeof(T).GetField(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + if (field != null) + { + GetValue = () => field.GetValue(target); + SetValue = value => field.SetValue(target, value); + return; + } + + var prop = typeof(T).GetProperty(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + if (prop != null) + { + GetValue = () => prop.GetValue(target, null); + SetValue = value => prop.SetValue(target, value, null); + return; + } + + throw new ArgumentException(string.Format("\"{0}\" not found : Type <{1}>", name, typeof(T).Name)); + } + } + + #region StructuralEqual + + /// Assert by deep recursive value equality compare + public static void IsStructuralEqual(this object actual, object expected, string message = "") + { + message = (string.IsNullOrEmpty(message) ? "" : ", " + message); + if (object.ReferenceEquals(actual, expected)) return; + + if (actual == null) throw new ArgumentNullException(nameof(actual), $"{nameof(actual)} is null{message}"); + if (expected == null) throw new ArgumentNullException(nameof(expected), $"{nameof(expected)} is null{message}"); + if (actual.GetType() != expected.GetType()) + { + var msg = string.Format("expected type is {0} but actual type is {1}{2}", + expected.GetType().Name, actual.GetType().Name, message); + throw new XunitException(msg); + } + + var r = StructuralEqual(actual, expected, new[] { actual.GetType().Name }); // root type + if (!r.IsEquals) + { + var msg = string.Format("is not structural equal, failed at {0}, actual = {1} expected = {2}{3}", + string.Join(".", r.Names), r.Left, r.Right, message); + throw new XunitException(msg); + } + } + + /// Assert by deep recursive value equality compare + public static void IsNotStructuralEqual(this object actual, object expected, string message = "") + { + message = (string.IsNullOrEmpty(message) ? "" : ", " + message); + if (object.ReferenceEquals(actual, expected)) throw new XunitException("actual is same reference" + message); ; + + if (actual == null) return; + if (expected == null) return; + if (actual.GetType() != expected.GetType()) + { + return; + } + + var r = StructuralEqual(actual, expected, new[] { actual.GetType().Name }); // root type + if (r.IsEquals) + { + throw new XunitException("is structural equal" + message); + } + } + + static EqualInfo SequenceEqual(IEnumerable leftEnumerable, IEnumerable rightEnumarable, IEnumerable names) + { + var le = leftEnumerable.GetEnumerator(); + using (le as IDisposable) + { + var re = rightEnumarable.GetEnumerator(); + + using (re as IDisposable) + { + var index = 0; + while (true) + { + object lValue = null; + object rValue = null; + var lMove = le.MoveNext(); + var rMove = re.MoveNext(); + if (lMove) lValue = le.Current; + if (rMove) rValue = re.Current; + + if (lMove && rMove) + { + var result = StructuralEqual(lValue, rValue, names.Concat(new[] { "[" + index + "]" })); + if (!result.IsEquals) + { + return result; + } + } + + if ((lMove == true && rMove == false) || (lMove == false && rMove == true)) + { + return new EqualInfo { IsEquals = false, Left = lValue, Right = rValue, Names = names.Concat(new[] { "[" + index + "]" }) }; + } + if (lMove == false && rMove == false) break; + index++; + } + } + } + return new EqualInfo { IsEquals = true, Left = leftEnumerable, Right = rightEnumarable, Names = names }; + } + + static EqualInfo StructuralEqual(object left, object right, IEnumerable names) + { + // type and basic checks + if (object.ReferenceEquals(left, right)) return new EqualInfo { IsEquals = true, Left = left, Right = right, Names = names }; + if (left == null || right == null) return new EqualInfo { IsEquals = false, Left = left, Right = right, Names = names }; + var lType = left.GetType(); + var rType = right.GetType(); + if (lType != rType) return new EqualInfo { IsEquals = false, Left = left, Right = right, Names = names }; + + var type = left.GetType(); + + // not object(int, string, etc...) + if (Type.GetTypeCode(type) != TypeCode.Object) + { + return new EqualInfo { IsEquals = left.Equals(right), Left = left, Right = right, Names = names }; + } + + // is sequence + if (typeof(IEnumerable).IsAssignableFrom(type)) + { + return SequenceEqual((IEnumerable)left, (IEnumerable)right, names); + } + + // IEquatable + var equatable = typeof(IEquatable<>).MakeGenericType(type); + if (equatable.IsAssignableFrom(type)) + { + var result = (bool)equatable.GetMethod("Equals").Invoke(left, new[] { right }); + return new EqualInfo { IsEquals = result, Left = left, Right = right, Names = names }; + } + + // is object + var fields = left.GetType().GetFields(BindingFlags.Instance | BindingFlags.Public); + var properties = left.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public).Where(x => x.GetGetMethod(false) != null); + var members = fields.Cast().Concat(properties); + + foreach (dynamic mi in fields.Cast().Concat(properties)) + { + var concatNames = names.Concat(new[] { (string)mi.Name }); + + object lv = mi.GetValue(left); + object rv = mi.GetValue(right); + var result = StructuralEqual(lv, rv, concatNames); + if (!result.IsEquals) + { + return result; + } + } + + return new EqualInfo { IsEquals = true, Left = left, Right = right, Names = names }; + } + + private class EqualInfo + { + public object Left; + public object Right; + public bool IsEquals; + public IEnumerable Names; + } + + #endregion + + #region DynamicAccessor + + /// to DynamicAccessor that can call private method/field/property/indexer. + public static dynamic AsDynamic(this T target) + { + return new DynamicAccessor(target); + } + + private class DynamicAccessor : DynamicObject + { + private readonly T target; + private static readonly BindingFlags TransparentFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; + + public DynamicAccessor(T target) + { + this.target = target; + } + + public override bool TrySetIndex(SetIndexBinder binder, object[] indexes, object value) + { + try + { + typeof(T).InvokeMember("Item", TransparentFlags | BindingFlags.SetProperty, null, target, indexes.Concat(new[] { value }).ToArray()); + return true; + } + catch (MissingMethodException) { throw new ArgumentException(string.Format("indexer not found : Type <{0}>", typeof(T).Name)); }; + } + + public override bool TryGetIndex(GetIndexBinder binder, object[] indexes, out object result) + { + try + { + result = typeof(T).InvokeMember("Item", TransparentFlags | BindingFlags.GetProperty, null, target, indexes); + return true; + } + catch (MissingMethodException) { throw new ArgumentException(string.Format("indexer not found : Type <{0}>", typeof(T).Name)); }; + } + + public override bool TrySetMember(SetMemberBinder binder, object value) + { + var accessor = new ReflectAccessor(target, binder.Name); + accessor.SetValue(value); + return true; + } + + public override bool TryGetMember(GetMemberBinder binder, out object result) + { + var accessor = new ReflectAccessor(target, binder.Name); + result = accessor.GetValue(); + return true; + } + + public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result) + { + var csharpBinder = binder.GetType().GetInterface("Microsoft.CSharp.RuntimeBinder.ICSharpInvokeOrInvokeMemberBinder"); + if (csharpBinder == null) throw new ArgumentException("is not csharp code"); + + var typeArgs = (csharpBinder.GetProperty("TypeArguments").GetValue(binder, null) as IList).ToArray(); + var parameterTypes = (binder.GetType().GetField("Cache", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(binder) as Dictionary) + .First() + .Key + .GetGenericArguments() + .Skip(2) + .Take(args.Length) + .ToArray(); + + var method = MatchMethod(binder.Name, args, typeArgs, parameterTypes); + result = method.Invoke(target, args); + + return true; + } + + private Type AssignableBoundType(Type left, Type right) + { + return (left == null || right == null) ? null + : left.IsAssignableFrom(right) ? left + : right.IsAssignableFrom(left) ? right + : null; + } + + private MethodInfo MatchMethod(string methodName, object[] args, Type[] typeArgs, Type[] parameterTypes) + { + // name match + var nameMatched = typeof(T).GetMethods(TransparentFlags) + .Where(mi => mi.Name == methodName) + .ToArray(); + if (!nameMatched.Any()) throw new ArgumentException(string.Format("\"{0}\" not found : Type <{1}>", methodName, typeof(T).Name)); + + // type inference + var typedMethods = nameMatched + .Select(mi => + { + var genericArguments = mi.GetGenericArguments(); + + if (!typeArgs.Any() && !genericArguments.Any()) // non generic method + { + return new + { + MethodInfo = mi, + TypeParameters = default(Dictionary) + }; + } + else if (!typeArgs.Any()) + { + var parameterGenericTypes = mi.GetParameters() + .Select(pi => pi.ParameterType) + .Zip(parameterTypes, Tuple.Create) + .GroupBy(a => a.Item1, a => a.Item2) + .Where(g => g.Key.IsGenericParameter) + .Select(g => new { g.Key, Type = g.Aggregate(AssignableBoundType) }) + .Where(a => a.Type != null); + + var typeParams = genericArguments + .GroupJoin(parameterGenericTypes, x => x, x => x.Key, (_, Args) => Args) + .ToArray(); + if (!typeParams.All(xs => xs.Any())) return null; // types short + + return new + { + MethodInfo = mi, + TypeParameters = typeParams + .Select(xs => xs.First()) + .ToDictionary(a => a.Key, a => a.Type) + }; + } + else + { + if (genericArguments.Length != typeArgs.Length) return null; + + return new + { + MethodInfo = mi, + TypeParameters = genericArguments + .Zip(typeArgs, Tuple.Create) + .ToDictionary(t => t.Item1, t => t.Item2) + }; + } + }) + .Where(a => a != null) + .Where(a => a.MethodInfo + .GetParameters() + .Select(pi => pi.ParameterType) + .SequenceEqual(parameterTypes, new EqualsComparer((x, y) => + (x.IsGenericParameter) + ? a.TypeParameters[x].IsAssignableFrom(y) + : x.Equals(y))) + ) + .ToArray(); + + if (!typedMethods.Any()) throw new ArgumentException(string.Format("\"{0}\" not match arguments : Type <{1}>", methodName, typeof(T).Name)); + + // nongeneric + var nongeneric = typedMethods.Where(a => a.TypeParameters == null).ToArray(); + if (nongeneric.Length == 1) return nongeneric[0].MethodInfo; + + // generic-- + var lessGeneric = typedMethods + .Where(a => !a.MethodInfo.GetParameters().All(pi => pi.ParameterType.IsGenericParameter)) + .ToArray(); + + // generic + var generic = (typedMethods.Length == 1) + ? typedMethods[0] + : (lessGeneric.Length == 1 ? lessGeneric[0] : null); + + if (generic != null) return generic.MethodInfo.MakeGenericMethod(generic.TypeParameters.Select(kvp => kvp.Value).ToArray()); + + // ambiguous + throw new ArgumentException(string.Format("\"{0}\" ambiguous arguments : Type <{1}>", methodName, typeof(T).Name)); + } + + private class EqualsComparer : IEqualityComparer + { + private readonly Func equals; + + public EqualsComparer(Func equals) + { + this.equals = equals; + } + + public bool Equals(TX x, TX y) + { + return equals(x, y); + } + + public int GetHashCode(TX obj) + { + return 0; + } + } + } + + #endregion + + #region ExpressionDumper + + private class ExpressionDumper : ExpressionVisitor + { + ParameterExpression param; + T target; + + public Dictionary Members { get; private set; } + + public ExpressionDumper(T target, ParameterExpression param) + { + this.target = target; + this.param = param; + this.Members = new Dictionary(); + } + + protected override System.Linq.Expressions.Expression VisitMember(MemberExpression node) + { + if (node.Expression == param && !Members.ContainsKey(node.Member.Name)) + { + var accessor = new ReflectAccessor(target, node.Member.Name); + Members.Add(node.Member.Name, accessor.GetValue()); + } + + return base.VisitMember(node); + } + } + + #endregion + } + + #endregion +} \ No newline at end of file diff --git a/src/ParserCombinator.Tests/CharParserTest.cs b/src/ParserCombinator.Tests/CharParserTest.cs new file mode 100644 index 0000000..32f3fcd --- /dev/null +++ b/src/ParserCombinator.Tests/CharParserTest.cs @@ -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(ParseResult result) => () => + { + var tmp = result.Result; + }; + } +} \ No newline at end of file diff --git a/src/ParserCombinator.Tests/ParserCombinator.Tests.csproj b/src/ParserCombinator.Tests/ParserCombinator.Tests.csproj new file mode 100644 index 0000000..6ec20ee --- /dev/null +++ b/src/ParserCombinator.Tests/ParserCombinator.Tests.csproj @@ -0,0 +1,14 @@ + + + netcoreapp2.0 + false + + + + + + + + + + \ No newline at end of file diff --git a/src/ParserCombinator.Tests/PostalCodeParser.cs b/src/ParserCombinator.Tests/PostalCodeParser.cs new file mode 100644 index 0000000..3d13cde --- /dev/null +++ b/src/ParserCombinator.Tests/PostalCodeParser.cs @@ -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 leftPart = Digit.Repeat(3).Map(chars => int.Parse(new string(chars.ToArray()))); + + // xxx-yyyy の yyyy 部分 + Parser rightPart = Digit.Repeat(4).Map(chars => int.Parse(new string(chars.ToArray()))); + + // xxx-yyyy の形式の郵便番号のパーサ + Parser postalCodeParser = leftPart + .Left(Literal('-')) + .Sequence(rightPart, (left, right) => new PostalCode(left, right)); + + ParseResult 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 leftPart = Digit.Repeat(3).Map(chars => int.Parse(new string(chars.ToArray()))); + +// xxx-yyyy の yyyy 部分 +Parser rightPart = Digit.Repeat(4).Map(chars => int.Parse(new string(chars.ToArray()))); + +// 普通の xxx-yyyy +Parser normal = leftPart.Left(Literal('-')).Sequence(rightPart, (l, r) => new PostalCode(l, r)); + +// xxxyyyy +Parser withoutSeparator = leftPart.Sequence(rightPart, (l, r) => new PostalCode(l, r)); + +Parser postalCode = normal.Or(withoutSeparator); + +// 〒 が付加されてもよい +Parser 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}"; + } +} \ No newline at end of file diff --git a/src/ParserCombinator.sln b/src/ParserCombinator.sln new file mode 100644 index 0000000..633ee59 --- /dev/null +++ b/src/ParserCombinator.sln @@ -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 diff --git a/src/ParserCombinator/CharParsers.cs b/src/ParserCombinator/CharParsers.cs new file mode 100644 index 0000000..ed9fdec --- /dev/null +++ b/src/ParserCombinator/CharParsers.cs @@ -0,0 +1,33 @@ +using System; + +namespace ParserCombinator +{ + using static ParseResultHelper; + + public static class CharParsers + { + public static Parser Any { get; } = (Source s) => + { + var (c, next) = s.Read(); + return Success(next, c); + }; + + public static Parser Digit { get; } = (Source s) => + { + var (c, next) = s.Read(); + return char.IsDigit(c) ? Success(next, c) : Failed(next, "Is not a digit."); + }; + + public static Parser Literal(char literal) => (Source s) => + { + var (c, next) = s.Read(); + return c == literal ? Success(next, c) : Failed(next, $"{c} is not equals {literal}"); + }; + + public static Parser Is(Func predicate) => (Source s) => + { + var (c, next) = s.Read(); + return predicate(c) ? Success(next, c) : Failed(next, $"predicate({c}) returns false."); + }; + } +} \ No newline at end of file diff --git a/src/ParserCombinator/Combinators.cs b/src/ParserCombinator/Combinators.cs new file mode 100644 index 0000000..2a31856 --- /dev/null +++ b/src/ParserCombinator/Combinators.cs @@ -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> Many(this Parser parser) +{ + ParseResult> Impl(Source s, ImmutableList results) + { + var result = parser(s); + + return result.IsSuccess + ? Impl(result.Source, results.Add(result.Result)) + : Success(s, results); + } + + return (Source s) => Impl(s, ImmutableList.Empty); +} + + public static Parser> Repeat(this Parser parser, int count) + { + ParseResult> Impl(Source s, int c, ImmutableList 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>(result.Source, result.Reason); + } + + return (Source s) => Impl(s, count, ImmutableList.Empty); + } + + public static Parser> Sequence(this Parser first, Parser second) => + first.Sequence(second, (f, s) => ImmutableList.Empty.Add(f).Add(s)); + + public static Parser> Sequence(this Parser> first, Parser second) => + first.Sequence(second, (f, s) => f.Add(s)); + + public static Parser Sequence(this Parser first, Parser second, Func 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(secondResult.Source, secondResult.Reason); + } + else + { + return Failed(firstResult.Source, firstResult.Reason); + } + }; + + public static Parser Or(this Parser left, Parser right) => (Source s) => + { + var leftResult = left(s); + + return leftResult.IsSuccess + ? leftResult + : right(s); + }; + + public static Parser Left(this Parser left, Parser right) => + left.Sequence(right, (l, r) => l); + + public static Parser Right(this Parser left, Parser right) => + left.Sequence(right, (l, r) => r); + + public static Parser Map(this Parser parser, Func mapper) => + (Source s) => + { + var result = parser(s); + return result.IsSuccess + ? Success(result.Source, mapper(result.Result)) + : Failed(result.Source, result.Reason); + }; + } +} \ No newline at end of file diff --git a/src/ParserCombinator/ParseResult.cs b/src/ParserCombinator/ParseResult.cs new file mode 100644 index 0000000..df3a173 --- /dev/null +++ b/src/ParserCombinator/ParseResult.cs @@ -0,0 +1,30 @@ +using System; + +namespace ParserCombinator +{ + public struct ParseResult + { + /// 実行後の Source + public Source Source { get; } + + /// 成功したかどうか + public bool IsSuccess { get; } + + /// パース結果 + 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; + } + } +} \ No newline at end of file diff --git a/src/ParserCombinator/ParseResultHelper.cs b/src/ParserCombinator/ParseResultHelper.cs new file mode 100644 index 0000000..2a8d441 --- /dev/null +++ b/src/ParserCombinator/ParseResultHelper.cs @@ -0,0 +1,11 @@ +namespace ParserCombinator +{ + public static class ParseResultHelper + { + public static ParseResult Success(Source source, T result) + => new ParseResult(source, true, result, default); + + public static ParseResult Failed(Source source, string reason) + => new ParseResult(source, false, default, reason); + } +} \ No newline at end of file diff --git a/src/ParserCombinator/Parser.cs b/src/ParserCombinator/Parser.cs new file mode 100644 index 0000000..2c4bd1c --- /dev/null +++ b/src/ParserCombinator/Parser.cs @@ -0,0 +1,4 @@ +namespace ParserCombinator +{ + public delegate ParseResult Parser(Source source); +} \ No newline at end of file diff --git a/src/ParserCombinator/ParserCombinator.csproj b/src/ParserCombinator/ParserCombinator.csproj new file mode 100644 index 0000000..0f07fed --- /dev/null +++ b/src/ParserCombinator/ParserCombinator.csproj @@ -0,0 +1,9 @@ + + + netstandard2.0 + latest + + + + + \ No newline at end of file diff --git a/src/ParserCombinator/Source.cs b/src/ParserCombinator/Source.cs new file mode 100644 index 0000000..9bbc7c9 --- /dev/null +++ b/src/ParserCombinator/Source.cs @@ -0,0 +1,79 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace ParserCombinator +{ + /// + /// Parser への入力 + /// + public struct Source + { + // 最初に与えられた文字列をもっておく + private readonly string _source; + + // 現在位置 + private readonly int _pos; + + private Source(string source, int pos) + { + _source = source; + _pos = pos; + } + + /// 文字列の先頭をさす Source を作成します + public static Source Create(string source) + => new Source(source, 0); + + /// 一文字読み出します + public (char c, Source source) Read() + { + if (_source.Length <= _pos) + { + // source の終わりを超えて読もうとした場合は Exception + throw new EndOfSourceException(this); + } + + return (_source[_pos], new Source(_source, _pos + 1)); + } + + /// 指定した文字数ぶん char を読み出します + public (IEnumerable chars, Source source) ReadChars(int count) + { + if (_source.Length < _pos + count) + { + // 読み出そうとしている長さが source をこえていたら Exception + throw new EndOfSourceException(this); + } + + return (_source.Skip(_pos).Take(count), new Source(_source, _pos + count)); + } + + /// 指定した長さの文字列を読み出します + public (string s, Source source) ReadString(int length) + { + if (_source.Length < _pos + length) + { + // 読み出そうとしている長さが source をこえていたら Exception + throw new EndOfSourceException(this); + } + + return (_source.Substring(_pos, length), new Source(_source, _pos + length)); + } + + /// Source の終わりに達したときの Exception + public class EndOfSourceException : Exception + { + private static readonly string EndOfSource = "EndOfSource"; + + /// 例外発生時の Source + public Source Source { get; } + + internal EndOfSourceException(Source source) + : base(EndOfSource) + { + this.Source = source; + } + } + } +} \ No newline at end of file