如何使用Microsoft DateTimeRecognizer提取日期范围中的单个日期文本?
如何使用Microsoft DateTimeRecognizer提取日期范围中的单个日期文本?
你遇到的问题很典型——Microsoft DateTimeRecognizer会将「3pm Monday to 3pm next Tuesday」这类日期范围识别为一个整体的范围类型实体(比如datetimeV2.datetimerange),而不是两个独立的日期/时间实体,所以你现有的逻辑会直接跳过这种情况。下面我来拆解下具体的解决方案,帮你扩展代码以支持提取范围中的单个日期文本和对应时间:
1. 先理解范围类型的识别结果结构
当Recognizer识别到日期/时间范围时,返回的ModelResult会有这些核心特征:
TypeName会是datetimeV2.daterange(日期范围)、datetimeV2.timerange(时间范围)或datetimeV2.datetimerange(日期时间范围)Children属性会包含两个子ModelResult,分别对应范围的起始端点和结束端点- 每个子
ModelResult的结构和你之前处理的单个日期/时间实体完全一致:TypeName为datetimeV2.date/time/datetime,Start和End标记了该端点在原字符串中的准确位置
2. 两种扩展方案(根据你的需求选择)
方案一:扩展函数,返回范围中的所有端点
如果你需要完整提取范围里的两个日期/时间(比如同时拿到「3pm Monday」和「3pm next Tuesday」),建议调整函数返回类型为包含多个结果的列表:
using System.Collections.Generic; using System.Linq; using Microsoft.Recognizers.Text; using Microsoft.Recognizers.Text.DateTime; using JetBrains.Annotations; public static class DateTimeExtensions { public static bool TryGetDateTimesAtStringStart( this string timexString, DateTime? referenceTime, [NotNullWhen(true)] out List<(DateTime? Date, string Text)>? dateTimeInfos) { dateTimeInfos = null; var results = DateTimeRecognizer.RecognizeDateTime( timexString, Culture.English, refTime: referenceTime ?? DateTime.Today); if (results.FirstOrDefault() is not ModelResult mainResult) { return false; } // 处理单个日期/时间/日期时间的原有逻辑 if (mainResult.Start == 0 && mainResult.TypeName is "datetimeV2.date" or "datetimeV2.time" or "datetimeV2.datetime") { var resolutionValues = (IList<Dictionary<string, string>>)mainResult.Resolution["values"]; var mainResolution = resolutionValues[0]; var dateTime = HandlePointInTime(mainResolution); if (dateTime != null) { dateTimeInfos = new List<(DateTime? Date, string Text)> { (dateTime, timexString.Substring(0, mainResult.End - mainResult.Start + 1)) }; return true; } return false; } // 新增:处理日期/时间范围的逻辑 if (mainResult.Start == 0 && mainResult.TypeName is "datetimeV2.daterange" or "datetimeV2.timerange" or "datetimeV2.datetimerange") { dateTimeInfos = new List<(DateTime? Date, string Text)>(); // 遍历范围的两个端点子实体 foreach (var childResult in mainResult.Children) { if (childResult.TypeName is "datetimeV2.date" or "datetimeV2.time" or "datetimeV2.datetime") { var resolutionValues = (IList<Dictionary<string, string>>)childResult.Resolution["values"]; var childResolution = resolutionValues[0]; var dateTime = HandlePointInTime(childResolution); if (dateTime != null) { // 从原字符串中提取该端点对应的文本 var endpointText = timexString.Substring(childResult.Start, childResult.End - childResult.Start + 1); dateTimeInfos.Add((dateTime, endpointText)); } } } return dateTimeInfos.Any(); } return false; } // 保留你原有的HandlePointInTime方法 private static DateTime? HandlePointInTime(Dictionary<string, string> resolution) { // 这里是你原有的解析逻辑,比如从timex或其他字段转换为DateTime // 示例实现(根据你的实际逻辑调整): if (resolution.TryGetValue("value", out string value) && DateTime.TryParse(value, out var dt)) { return dt; } return null; } }
方案二:保持原函数签名,仅提取范围的第一个端点
如果你希望原函数的输入输出结构不变,只需要拿到范围中的第一个日期/时间(比如「3pm Monday to 3pm next Tuesday」里的「3pm Monday」),可以修改原函数的判断逻辑:
using System.Linq; using Microsoft.Recognizers.Text; using Microsoft.Recognizers.Text.DateTime; using JetBrains.Annotations; public static class DateTimeExtensions { public static bool TryGetDateTimeAtStringStart( this string timexString, DateTime? referenceTime, [NotNullWhen(true)] out DateTime? dateTime, [NotNullWhen(true)] out string? text) { dateTime = null; text = null; var results = DateTimeRecognizer.RecognizeDateTime( timexString, Culture.English, refTime: referenceTime ?? DateTime.Today); if (results.FirstOrDefault() is not ModelResult mainResult) { return false; } // 原有单个日期/时间的处理逻辑 if (mainResult.Start == 0 && mainResult.TypeName is "datetimeV2.date" or "datetimeV2.time" or "datetimeV2.datetime") { var resolutionValues = (IList<Dictionary<string, string>>)mainResult.Resolution["values"]; var mainResolution = resolutionValues[0]; dateTime = HandlePointInTime(mainResolution); if (dateTime != null) { text = timexString.Substring(0, mainResult.End - mainResult.Start + 1); return true; } return false; } // 新增:处理范围类型,提取第一个端点 if (mainResult.Start == 0 && mainResult.TypeName is "datetimeV2.daterange" or "datetimeV2.timerange" or "datetimeV2.datetimerange") { // 找到范围中的第一个有效日期/时间端点 var firstEndpoint = mainResult.Children.FirstOrDefault( c => c.TypeName is "datetimeV2.date" or "datetimeV2.time" or "datetimeV2.datetime"); if (firstEndpoint != null) { var resolutionValues = (IList<Dictionary<string, string>>)firstEndpoint.Resolution["values"]; var childResolution = resolutionValues[0]; dateTime = HandlePointInTime(childResolution); if (dateTime != null) { text = timexString.Substring(firstEndpoint.Start, firstEndpoint.End - firstEndpoint.Start + 1); return true; } } } return false; } private static DateTime? HandlePointInTime(Dictionary<string, string> resolution) { // 你的原有解析逻辑,示例实现: if (resolution.TryGetValue("value", out string value) && DateTime.TryParse(value, out var dt)) { return dt; } return null; } }
3. 关键注意事项
- 确保你的
HandlePointInTime方法能兼容子端点的Resolution数据(格式和单个日期的Resolution完全一致,无需额外修改) - DateTimeRecognizer会自动处理各种范围分隔符(比如
to、until、from...to...、-等),Children属性会准确对应每个端点的位置 - 如果需要支持非英语场景,记得将
Culture.English替换为对应的文化信息(比如Culture.Chinese)
内容来源于Stack Exchange
相关产品推荐
相关产品推荐

