CollectionRegex -- 集合的正则表达式
使用熟悉的语言在对象序列中查找模式。
先决条件
阅读本文的读者应了解正则表达式的基础知识。本文不解释正则表达式。 Regular Expression .info 网站是正则表达式的一个很好的资源,既可以作为入门点,也可以作为参考。
引言
本文介绍了一个简单、强类型的类库,它提供了在对象序列中查找模式的方法。模式用正则表达式描述,序列是实现 IEnumerable<T>
泛型接口的任何类的实例 [msdn]。这包括众所周知的 .NET Framework 类,如 List<T>
、HashSet<T>
或 Dictionary<K,V>
,以及 LINQ 查询返回的对象。
Using the Code
可以通过调用 CollectionRegex.Match
方法来访问库的功能。此方法提供实例和静态版本。为了使其可用,需要以下信息:
- 一个由正则表达式表示的模式,
- 模式中的字符与集合项之间的关系,
- (可选)要传递给 .NET 正则表达式引擎的 选项。
当所有上述参数都已知后,就可以调用 Match
方法并获取结果。Match
方法返回所有匹配项的枚举,即 IEnumerable<CollectionMatch<T>>
。实现使用了 yield return
[msdn] 构造,因此操作是按需执行的。该库支持命名和未命名捕获组。
示例 1
以下代码分析数字序列,以查找至少三个连续数字超出指定范围的位置。每个数字被归类为三个类别之一:“太低”(a
)、“正常”(b
)或“太高”(c
)。将使用的正则表达式是:
[^b]{3,}
b
字符的子序列。就集合而言,它匹配三个或更多连续数字,这些数字**不**属于“正常”类别。由于定义的类别是 a、b 和 c,因此该表达式等同于 [ac]{3,}
。所需的 .NET 代码非常直接。当然,模式中的字符与类别之间的关系需要定义。在此示例中,它通过 lambda 表达式完成。它们是匿名方法,接受集合中的每个数字并返回一个布尔值,当数字属于特定类别时,该布尔值为 true。// create a regex object
var reg = new CollectionRegex<double>(@"[^b]{3,}");
double low = 3,
high = 7;
// define predicates for "low", "ok" and "high"
// with lambda expressions
reg.AddPredicate(s => s <= low, 'a');
reg.AddPredicate(s => s > low && s < high, 'b');
reg.AddPredicate(s => s >= high, 'c');
测试代码// create a test sequence var collection = new List<double> { 4, 5, 9, 6, 7, 8, 9, 8, 6, 4, 3, 2, 4, 2, 2, 3, 3, 5, 5, 5, 3, 2, 7, 5 }; // run the regex var actual = reg.Match(collection).ToList(); // check if results are as expected Assert.AreEqual(3, actual.Count()); Assert.AreEqual(4, actual[0].Index); Assert.AreEqual(4, actual[0].Count); Assert.AreEqual(13, actual[1].Index); Assert.AreEqual(4, actual[1].Count); Assert.AreEqual(20, actual[2].Index); Assert.AreEqual(3, actual[2].Count);
测试集合在内部由字符串“bbcb cccc bbaa baaa abbb aacb
”(不带空格)表示。正则表达式匹配三个子序列:{7, 8, 9, 8}(cccc
)、{2, 2, 3, 3}(aaaa
)和 {3, 2, 7}(aac
)。
LINQ 替代方案
对于那些以 C# 思考的人来说,通过检查不使用 CollectionRegex 库的实现来理解这个想法可能会更容易。List<IEnumerable<double>> results = new List<IEnumerable<double>>(); for (int i = 0; i < collection.Count; i++) { IEnumerable<double> enu = collection.Skip(i); enu = enu.TakeWhile(x => x <= low || x >= high); if (enu.Count() >= 3) { results.Add(enu); i += enu.Count() - 1; } }
示例 2
以下代码查找生产过程中未成功完成的请求。集合包含 ProductionEvent
类型的对象,这些对象表示一个项目(r
)、一个成功报告(s
)或一个失败报告(f
)的请求。如果项目未能生产,则将失败报告放入集合中。一个项目即使失败了几次也可以成功生产。正则表达式旨在查找根本没有生产出来的项目。
(?<item>r)f+(?=r|$)
(?<item>r)
匹配一个项目请求,并定义一个名为item
的命名组。f+
匹配一个或多个失败报告。- 最后,最后一部分
(?=r|$)
进行一个 正向先行断言,查找另一个请求(r
)或集合的结尾($
),以确保在分析的请求之后没有成功的报告。
例如,在序列 rs rfff rffffs rff
中,它会找到第二个和第四个请求(rfff
和 rff
)。但是,它不会匹配第三个(rffffs
),因为它最终成功完成了,尽管过程中有四次失败。源代码假定存在一个名为 ProductionEvent
的类,该类公开两个属性:Type
和 ItemName
。
var reg = new CollectionRegex<ProductionEvent>(@"(?<item>r)f+(?=r|$)");
reg.AddPredicate(s => s.Type == ProductionEventTypes.Success, 's');
reg.AddPredicate(s => s.Type == ProductionEventTypes.ItemRequest, 'r');
reg.AddPredicate(s => s.Type == ProductionEventTypes.Failure, 'f');
以及测试代码var collection = new List<ProductionEvent> { new ProductionEvent { Type = ProductionEventTypes.ItemRequest, ItemName="chocolade" }, new ProductionEvent { Type= ProductionEventTypes.Success }, new ProductionEvent { Type = ProductionEventTypes.ItemRequest, ItemName="impossible1" }, new ProductionEvent { Type= ProductionEventTypes.Failure }, new ProductionEvent { Type= ProductionEventTypes.Failure }, new ProductionEvent { Type= ProductionEventTypes.Failure }, new ProductionEvent { Type = ProductionEventTypes.ItemRequest, ItemName="problematic" }, new ProductionEvent { Type= ProductionEventTypes.Failure }, new ProductionEvent { Type= ProductionEventTypes.Failure }, new ProductionEvent { Type= ProductionEventTypes.Failure }, new ProductionEvent { Type= ProductionEventTypes.Failure }, new ProductionEvent { Type= ProductionEventTypes.Success }, new ProductionEvent { Type = ProductionEventTypes.ItemRequest, ItemName="impossible2" }, new ProductionEvent { Type= ProductionEventTypes.Failure }, }; var actual = reg.Match(collection).ToList(); foreach (CollectionMatch<ProductionEvent> e in actual) { // access a named group "item" Assert.IsTrue(e.Groups["item"].Single().ItemName.StartsWith("impossible")); }
LINQ 替代方案
在下面的代码片段中,requests
列表模拟了一个捕获组“item”。
List<IEnumerable<ProductionEvent>> results = new List<IEnumerable<ProductionEvent>>(); List<ProductionEvent> requests = new List<ProductionEvent>(); for (int i = 0; i < collection.Count; i++) { IEnumerable<ProductionEvent> begin = collection.Skip(i); IEnumerable<ProductionEvent> enu = begin; if (enu.Count() >= 2) { var request = enu.First(); if (request.Type == ProductionEventTypes.ItemRequest) { enu = enu.Skip(1); var x = enu.First(); if (x.Type == ProductionEventTypes.Failure) { enu = enu.SkipWhile(p => p.Type == ProductionEventTypes.Failure); if (enu.Count() == 0) { results.Add(begin); requests.Add(request); i += begin.Count(); } else if (enu.First().Type == ProductionEventTypes.ItemRequest) { // (dirty) var result = begin.TakeWhile(p => !object.ReferenceEquals(p, enu.First())); results.Add(result); requests.Add(request); i += result.Count(); } } } } }
备注
谓词必须是互斥的。如果任何项匹配多个谓词,则 Match
方法将抛出 ArgumentException。
谓词是继承自抽象类 CollectionRegexPredicate
的类的实例。如果你想提供一种自定义机制来检查一个对象是否属于某个类别,你所要做的就是实现 IsMatch(T obj)
方法。
未被任何谓词匹配的项在正则表达式中用逗号(','
)表示。因此,像 [^ab]
这样的否定组将匹配这些项。在经过长时间的思考后选择了逗号,在 ASCII 表中没有其他字符可以准确地代表这些项。(第一个可打印、无问题且易于人类计数的字符)。
除了常规的类库,我还为 IEnumerable
类编写了一个扩展方法,它允许使用如下构造:
var predicates = new CollectionRegexDelegatePredicate<double>[] {
new CollectionRegexDelegatePredicate<double>(s => s <= low, 'a'),
new CollectionRegexDelegatePredicate<double>(s => s > low && s < high, 'b'),
new CollectionRegexDelegatePredicate<double>(s => s >= high, 'c'),
};
var actual = collection.MatchRegex(predicates, @"[^b]{3,}")s;
请使用文章下方的消息板报告错误或提交功能请求。
历史
- 2012-04-14 -- 文章和 API 更新
- 2012-04-11 -- 发布了原始版本