diff --git a/RegExtract.Test/Usage.cs b/RegExtract.Test/Usage.cs index af2e08c..bb3db1e 100644 --- a/RegExtract.Test/Usage.cs +++ b/RegExtract.Test/Usage.cs @@ -16,6 +16,62 @@ public class Usage const string pattern_nested = "(((.)(.)(.)(.)(.)(.)(.)(.)(.)))"; const string pattern_named = "(?(?(?.)(?.)(?.)(?.)(?.)(?.)(?.)(?.)(?.)))"; + [Fact] + public void can_try_extract_to_tuple_using_extension() + { + var ok = data.TryExtract<(int a, char b, string c, int d, char e, string f, int g, char h, string i)>(pattern, out var extracted); + + Assert.True(ok); + + Assert.IsType(extracted.a); + Assert.IsType(extracted.b); + Assert.IsType(extracted.c); + Assert.IsType(extracted.d); + Assert.IsType(extracted.e); + Assert.IsType(extracted.f); + Assert.IsType(extracted.g); + Assert.IsType(extracted.h); + Assert.IsType(extracted.i); + + Assert.Equal(1, extracted.a); + Assert.Equal('2', extracted.b); + Assert.Equal("3", extracted.c); + Assert.Equal(4, extracted.d); + Assert.Equal('5', extracted.e); + Assert.Equal("6", extracted.f); + Assert.Equal(7, extracted.g); + Assert.Equal('8', extracted.h); + Assert.Equal("9", extracted.i); + } + [Fact] + public void can_try_extract_to_tuple() + { + var plan = ExtractionPlan<(int a, char b, string c, int d, char e, string f, int g, char h, string i)>.CreatePlan(new Regex(pattern)); + var ok = plan.TryExtract(data, out var extracted); + + Assert.True(ok); + + Assert.IsType(extracted.a); + Assert.IsType(extracted.b); + Assert.IsType(extracted.c); + Assert.IsType(extracted.d); + Assert.IsType(extracted.e); + Assert.IsType(extracted.f); + Assert.IsType(extracted.g); + Assert.IsType(extracted.h); + Assert.IsType(extracted.i); + + Assert.Equal(1, extracted.a); + Assert.Equal('2', extracted.b); + Assert.Equal("3", extracted.c); + Assert.Equal(4, extracted.d); + Assert.Equal('5', extracted.e); + Assert.Equal("6", extracted.f); + Assert.Equal(7, extracted.g); + Assert.Equal('8', extracted.h); + Assert.Equal("9", extracted.i); + } + [Fact] public void can_parse_lookbehind() { @@ -140,7 +196,7 @@ record PropertiesRecord } // Don't currently handle nested named captures, and I'm not sure we ever will. - //[Fact] + // [Fact] //public void can_extract_named_capture_groups_to_properties() //{ // PropertiesRecord? record = data.Extract(pattern_named); diff --git a/RegExtract/ExtractionPlan.cs b/RegExtract/ExtractionPlan.cs index ec70ae5..29736c3 100644 --- a/RegExtract/ExtractionPlan.cs +++ b/RegExtract/ExtractionPlan.cs @@ -31,6 +31,28 @@ public T Extract(Match match) return (T)Plan.Execute(match)!; } + public bool TryExtract(string str, out T result) + { + result = default!; + if (!Plan.TryExecute(_tree?.Regex.Match(str) ?? Regex.Match("",""), out var temp)) + { + return false; + } + result = (T)temp!; + return true; + } + + public bool TryExtract(Match match, out T result) + { + result = default!; + if (!Plan.TryExecute(match, out var temp)) + { + return false; + } + result = (T)temp!; + return true; + } + static public ExtractionPlan CreatePlan(Regex regex, RegExtractOptions reOptions= RegExtractOptions.None) { ExtractionPlan plan = new ExtractionPlan(); diff --git a/RegExtract/ExtractionPlanNode.cs b/RegExtract/ExtractionPlanNode.cs index afa49e8..26cde2f 100644 --- a/RegExtract/ExtractionPlanNode.cs +++ b/RegExtract/ExtractionPlanNode.cs @@ -146,11 +146,83 @@ internal virtual void Validate() return; } + internal virtual bool TryConstruct(Match match, Type type, (string Value, int Index, int Length) range, out object? result) + { + result = Construct(match, type, range); + return true; + } + internal virtual object? Construct(Match match, Type type, (string Value, int Index, int Length) range) { throw new InvalidOperationException("Can't construct a node based on base ExtractionPlanNode type."); } + internal virtual bool TryExecute(Match match, int captureStart, int captureLength, out object? result) + { + var ranges = AsEnumerable(match.Groups[groupName].Captures) + .Where(cap => cap.Index >= captureStart && cap.Index + cap.Length <= captureStart + captureLength) + .Select(cap => (cap.Value, cap.Index, cap.Length)); + Type innerType = IsNullable(type) ? type.GetGenericArguments().Single() : type; + bool isCollection = IsCollection(type); + if (!isCollection) + { + if (!ranges.Any()) + { + if (type.IsClass || Nullable.GetUnderlyingType(type) != null) + { + result = null; + return false; + } + result = Convert.ChangeType(null, type); + return true; + } + else + { + var lastRange = ranges.Last(); + if (!TryConstruct(match, innerType, lastRange, out result)) + { + return false; + } + foreach (var prop in propertyNodes) + { + result!.GetType().GetProperty(prop.groupName).GetSetMethod().Invoke(result, new[] { prop.Execute(match, lastRange.Index, lastRange.Length) }); + } + } + } + else + { + result = null; + var itemType = type.GetGenericArguments().Single(); + var vals = Activator.CreateInstance(type); + var addMethod = type.GetMethod("Add"); + foreach (var range in ranges) + { + if (!TryConstruct(match, itemType, range, out var itemVal)) + { + return false; + } + foreach (var prop in propertyNodes) + { + itemVal!.GetType().GetProperty(prop.groupName).GetSetMethod().Invoke(result, new[] { prop.Execute(match, range.Index, range.Length) }); + } + addMethod.Invoke(vals, new[] { itemVal }); + } + + result = vals; + } + return true; + } + + public bool TryExecute(Match match, out object? result) + { + if (!match.Success) + { + throw new ArgumentException("Regex didn't match."); + } + + return TryExecute(match, match.Groups[groupName].Index, match.Groups[groupName].Length, out result); + } + internal virtual object? Execute(Match match, int captureStart, int captureLength) { object? result = null; diff --git a/RegExtract/ExtractionPlanNodeTypes.cs b/RegExtract/ExtractionPlanNodeTypes.cs index e9d0375..88861d9 100644 --- a/RegExtract/ExtractionPlanNodeTypes.cs +++ b/RegExtract/ExtractionPlanNodeTypes.cs @@ -104,6 +104,11 @@ internal override void Validate() internal record EnumParseNode(string groupName, Type type, ExtractionPlanNode[] constructorParams, ExtractionPlanNode[] propertySetters) : ExtractionPlanNode(groupName, type, constructorParams, propertySetters) { + internal override bool TryConstruct(Match match, Type type, (string Value, int Index, int Length) range, out object? result) + { + result = Construct(match, type, range); + return true; + } internal override object? Construct(Match match, Type type, (string Value, int Index, int Length) range) { return Enum.Parse(type, range.Value); @@ -138,6 +143,27 @@ internal override void Validate() internal record StaticParseMethodNode(string groupName, Type type, ExtractionPlanNode[] constructorParams, ExtractionPlanNode[] propertySetters) : ExtractionPlanNode(groupName, type, constructorParams, propertySetters) { + internal override bool TryConstruct(Match match, Type type, (string Value, int Index, int Length) range, out object? result) + { + type = IsCollection(type) ? type.GetGenericArguments().Single() : type; + type = IsNullable(type) ? type.GetGenericArguments().Single() : type; + if (type.Namespace != "System") + { + result = Construct(match, type, range); + return true; + } + + var args = new object[] { range.Value, null! }; + var ok = (bool)type.GetMethod( + "TryParse", + BindingFlags.Static | BindingFlags.Public, + null, + new Type[] { typeof(string), Type.GetType($"{type.FullName}&") }, + null + ).Invoke(null, args); + result = args[1]; + return ok; + } internal override object? Construct(Match match, Type type, (string Value, int Index, int Length) range) { type = IsCollection(type) ? type.GetGenericArguments().Single() : type; diff --git a/RegExtract/RegExtractExtensions.cs b/RegExtract/RegExtractExtensions.cs index 73fa7f7..867766b 100644 --- a/RegExtract/RegExtractExtensions.cs +++ b/RegExtract/RegExtractExtensions.cs @@ -81,11 +81,87 @@ public static IEnumerable Extract(this IEnumerable str, RegExtract var rx = GetRegexFromType(typeof(T)); return Extract(str, rx, options); } - + public static IEnumerable Extract(this IEnumerable str, Regex rx, RegExtractOptions options = RegExtractOptions.None) { var plan = ExtractionPlan.CreatePlan(rx, options); return str.Select(s => plan.Extract(rx.Match(s))); } + + public static bool TryExtract(this string str, string rx, out T result, RegExtractOptions options = RegExtractOptions.None) + { + return TryExtract(str, rx, RegexOptions.None, out result, options); + } + + public static bool TryExtract(this string str, string rx, RegexOptions rxOptions, out T result, RegExtractOptions options = RegExtractOptions.None) + { + var match = Regex.Match(str, rx, rxOptions); + + var plan = ExtractionPlan.CreatePlan(new Regex(rx)); + return plan.TryExtract(match, out result); + } + + public static bool TryExtract(this string str, Regex rx, out T result, RegExtractOptions options = RegExtractOptions.None) + { + var match = rx.Match(str); + var plan = ExtractionPlan.CreatePlan(rx); + return plan.TryExtract(match, out result); + } + + public static bool TryExtract(this string str, ExtractionPlan plan, out T result) + { + return plan.TryExtract(str, out result); + } + + public static bool TryExtract(this string str, out T result, RegExtractOptions options = RegExtractOptions.None) + { + return TryExtract(str, GetRegexFromType(typeof(T)), out result, options); + } + + public static bool TryExtract(this IEnumerable str, string rx, out IEnumerable result, RegExtractOptions options = RegExtractOptions.None) + { + return TryExtract(str, rx, RegexOptions.None, out result, options); + } + + public static bool TryExtract(this IEnumerable str, string rx, RegexOptions rxOptions, out IEnumerable result, RegExtractOptions options = RegExtractOptions.None) + { + return TryExtract(str, new Regex(rx, rxOptions), out result, options); + } + + public static bool TryExtract(this IEnumerable str, ExtractionPlan plan, out IEnumerable result) + { + var anyFailure = false; + result = str.Select(s => + { + if (plan.TryExtract(s, out var result)) + { + return result; + } + anyFailure = true; + return default!; + }); + return anyFailure; + } + + public static bool TryExtract(this IEnumerable str, out IEnumerable result, RegExtractOptions options = RegExtractOptions.None) + { + return TryExtract(str, GetRegexFromType(typeof(T)), out result, options); + } + + public static bool TryExtract(this IEnumerable str, Regex rx, out IEnumerable result, RegExtractOptions options = RegExtractOptions.None) + { + var plan = ExtractionPlan.CreatePlan(rx, options); + var anyFailure = false; + result = str.Select(s => + { + if (plan.TryExtract(rx.Match(s), out var result)) + { + return result; + } + anyFailure = true; + return default!; + }); + return anyFailure; + } } }