在自然语音中识别日期和时间


挑战赛


哈Ha! 我开始对爱丽丝的技能感兴趣,并开始思考它们可以带来什么好处。 该网站上有很多很酷的游戏(包括我的),但是我想制作一个语音性能真正需要的工作工具,而不仅仅是复制带有按钮的现有聊天机器人。


双手忙或需要执行许多连续操作(尤其是在电话屏幕上)时,语音才有意义。 因此出现了一种技巧的想法,该技巧通过一个命令从文本中提取日期和时间的指示,并将带有该文本的事件添加到Google日历 。 例如,如果用户说后天晚上11点会有一个美丽的日落 ,则行中的后天下午23:00 将有一个美丽的日落


猫下是Hors库算法的描述:自然俄语语音中的日期和时间识别器。 马是斯拉夫的太阳神。


Github | 努吉特


现有解决方案


Python中的dateparser
声明了对俄语的支持,但是在俄语中,该库甚至无法应付基本情况:


>>> import dateparser >>> dateparser.parse(u'13  2015 .  13:34') datetime.datetime(2015, 1, 13, 13, 34) >>> dateparser.parse(u' ') >>> dateparser.parse(u'    9 ') >>> dateparser.parse(u'13 ') datetime.datetime(2019, 10, 13, 0, 0) >>> dateparser.parse(u'13   9 ') 

长期使用Ruby
他们说,这是一个专为红论者所熟知的图书馆,可以很好地完成工作。 但是找不到对俄罗斯的支持。


Google助手
由于我们正在谈论向Google日历添加语音,因此我们问为什么不使用助手? 有可能,但是该项目的想法是用一个短语完成工作,而无需不必要的手势和轻敲。 并做到可靠。 助理暂时有此问题:



预设值


我在.NETStandard 2.0(C#)上编写了一个库。 由于该库最初是为Alice创建的,因此文本中的所有数字都应为数字,因为Alice会自动执行此转换。 如果您在字符串中包含数字,那么有一篇 不错的Doomer3D 文章介绍如何将单词转换为数字。


形态学


使用语音输入时,区分正确单词和错误单词的最可靠方法是使用单词形式。 我和一个朋友在Yandex 的Alice School视频教程中对此进行了更详细的讨论。 在本文中,我将在后台处理单词形式。 这样的构造将出现在代码中:


 Morph.HasOneOfLemmas(t, "", "", ""); 

如果单词t是以下三个单词之一的任何形式,则此函数返回true ,例如: pastpastprevious


理论


要了解如何在文本中捕捉日期,您需要列出用于在实际对话中指示日期的典型短语。 例如:


- 我明天去散步
- 明天晚上我去散散步
- 下星期四我要去看电影
- 下周四晚上9点我去电影院
- 3月21日上午10点开会


我们看到单词整体上分为三种类型:


  1. 那些总是指日期和时间的名称(月和日的名称)
  2. 与日期和时间在相对于其他字词的某个位置(“天”,“晚上”,“下一个”,数字)相关的那些
  3. 那些永远不会约会的人

对于第一个和最后一个,一切都很清楚,但是对于第二个,则有困难。 该算法的原始版本是一个糟糕的意大利面条代码,带有大量的if ,因为我试图考虑所需单词的所有可能组合和排列,但是后来我想出了一个更好的选择。 事实是,人类已经发明了一个系统,该系统可让您快速轻松地考虑字符的排列和组合:正则表达式引擎。


烹饪线


我们将行拆分为标记,删除标点符号并将所有内容都减小为小写。 之后,我们将每个非空标记替换为单个字符,以使其更易于使用正则表达式。


令牌(单词)替换符号
“年”ÿ
月份名称中号
工作日名称d
回去b
“以后”l(下层L)
“通过”
“休息日”w ^
分钟Ë
“小时”^ h
“天”d
“周”w
“过去”,“过去”,“上一个”s
“此”,“当前”,“当前”ü
“最近”,“未来”ÿ
“下一个”,“未来”X
“后天”6
明天5
今天4
昨天3
“前天”2
早上[R
中午ñ
“晚上”v
“晚上”g ^
一半^ h
四分之一
c,c˚F
“到”,“通过”Ť
关于
“数字”
“和”ñ
大于1900且小于9999的数字1个
小于1901的非负数0
该算法已处理的日期@
任何其他令牌_

ParserExtractors.cs
内部 静态 ParserExtractors
{
内部 静态 字符串 CreatePatternFromToken字符串 令牌
{
var t = token.ToLower().Replace("[^0-9--]", "").Trim();
if (Morph.HasOneOfLemmas(t, Keywords.Year)) return "Y";
if (Morph.HasOneOfLemmas(t, Keywords.Months().ToArray())) return "M";
if (Morph.HasOneOfLemmas(t, Keywords.DaysOfWeek().ToArray())) return "D";
if (Morph.HasOneOfLemmas(t, Keywords.PreviousPostfix)) return "b";
if (Morph.HasOneOfLemmas(t, Keywords.AfterPostfix)) return "l";
if (Morph.HasOneOfLemmas(t, Keywords.After)) return "i";
if (Morph.HasOneOfLemmas(t, Keywords.Holiday)) return "W";
var p = PeriodFromToken(t);
switch (p)
{
case Period.Minute:
return "e";
case Period.Hour:
return "h";
case Period.Day:
return "d";
case Period.Week:
return "w";
case Period.Month:
return "m";
}
var r = RelativeModeFromToken(t);
switch (r)
{
case RelativeMode.Previous:
return "s";
case RelativeMode.Current:
return "u";
case RelativeMode.CurrentNext:
return "y";
case RelativeMode.Next:
return "x";
}
var n = NeighbourDaysFromToken(t);
if (n > int.MinValue)
{
return (n + 4).ToString();
}
var d = DaytimeFromToken(t);
switch (d)
{
case DayTime.Morning:
return "r";
case DayTime.Noon:
return "n";
case DayTime.Day:
return "a";
case DayTime.Evening:
return "v";
case DayTime.Night:
return "g";
}
var pt = PartTimeFromToken(t);
switch (pt)
{
case PartTime.Quarter:
return "Q";
case PartTime.Half:
return "H";
}
if (int.TryParse(t, out var c))
{
if (c < 0 || c > 9999) return "_";
if (c > 1900) return "1";
return "0";
}
if (Morph.HasOneOfLemmas(t, Keywords.TimeFrom)) return "f";
if (Morph.HasOneOfLemmas(t, Keywords.TimeTo)) return "t";
if (Morph.HasOneOfLemmas(t, Keywords.TimeOn)) return "o";
if (Morph.HasOneOfLemmas(t, Keywords.DayInMonth)) return "#";
if (t == "") return "N";
return "_";
}
private static PartTime PartTimeFromToken(string t)
{
if (Morph.HasOneOfLemmas(t, Keywords.Quarter)) return PartTime.Quarter;
if (Morph.HasOneOfLemmas(t, Keywords.Half)) return PartTime.Half;
return PartTime.None;
}
private static DayTime DaytimeFromToken(string t)
{
if (Morph.HasOneOfLemmas(t, Keywords.Noon)) return DayTime.Noon;
if (Morph.HasOneOfLemmas(t, Keywords.Morning)) return DayTime.Morning;
if (Morph.HasOneOfLemmas(t, Keywords.Evening)) return DayTime.Evening;
if (Morph.HasOneOfLemmas(t, Keywords.Night)) return DayTime.Night;
if (Morph.HasOneOfLemmas(t, Keywords.DaytimeDay)) return DayTime.Day;
return DayTime.None;
}
private static Period PeriodFromToken(string t)
{
if (Morph.HasOneOfLemmas(t, Keywords.Year)) return Period.Year;
if (Morph.HasOneOfLemmas(t, Keywords.Month)) return Period.Month;
if (Morph.HasOneOfLemmas(t, Keywords.Week)) return Period.Week;
if (Morph.HasOneOfLemmas(t, Keywords.Day)) return Period.Day;
if (Morph.HasOneOfLemmas(t, Keywords.Hour)) return Period.Hour;
if (Morph.HasOneOfLemmas(t, Keywords.Minute)) return Period.Minute;
return Period.None;
}
private static int NeighbourDaysFromToken(string t)
{
if (Morph.HasOneOfLemmas(t, Keywords.Tomorrow)) return 1;
if (Morph.HasOneOfLemmas(t, Keywords.Today)) return 0;
if (Morph.HasOneOfLemmas(t, Keywords.AfterTomorrow)) return 2;
if (Morph.HasOneOfLemmas(t, Keywords.Yesterday)) return -1;
if (Morph.HasOneOfLemmas(t, Keywords.BeforeYesterday)) return -2;
return int.MinValue;
}
internal static RelativeMode RelativeModeFromToken(string t)
{
if (Morph.HasOneOfLemmas(t, Keywords.Current)) return RelativeMode.Current;
if (Morph.HasOneOfLemmas(t, Keywords.Next)) return RelativeMode.Next;
if (Morph.HasOneOfLemmas(t, Keywords.Previous)) return RelativeMode.Previous;
if (Morph.HasOneOfLemmas(t, Keywords.CurrentNext)) return RelativeMode.CurrentNext;
return RelativeMode.None;
}
}

关键字
public class Keywords
{
public static readonly string[] After = {""};
public static readonly string[] AfterPostfix = {""};
public static readonly string[] PreviousPostfix = {""};
public static readonly string[] Next = {"", ""};
public static readonly string[] Previous = {"", "", ""};
public static readonly string[] Current = {"", "", ""};
public static readonly string[] CurrentNext = {"", ""};
public static readonly string[] Today = {""};
public static readonly string[] Tomorrow = {""};
public static readonly string[] AfterTomorrow = {""};
public static readonly string[] Yesterday = {""};
public static readonly string[] BeforeYesterday = {""};
public static readonly string[] Holiday = {""};
public static readonly string[] Second = {"", ""};
public static readonly string[] Minute = {"", ""};
public static readonly string[] Hour = {"", ""};
public static readonly string[] Day = {""};
public static readonly string[] Week = {""};
public static readonly string[] Month = {"", ""};
public static readonly string[] Year = {""};
public static readonly string[] Noon = {""};
public static readonly string[] Morning = {""};
public static readonly string[] Evening = {""};
public static readonly string[] Night = {""};
public static readonly string[] Half = {"", ""};
public static readonly string[] Quarter = {""};
public static readonly string[] DayInMonth = {""};
public static readonly string[] January = {"", ""};
public static readonly string[] February = {"", ""};
public static readonly string[] March = {"", ""};
public static readonly string[] April = {"", ""};
public static readonly string[] May = {"", ""};
public static readonly string[] June = {"", ""};
public static readonly string[] July = {"", ""};
public static readonly string[] August = {"", ""};
public static readonly string[] September = {"", "", ""};
public static readonly string[] October = {"", ""};
public static readonly string[] November = {"", "", ""};
public static readonly string[] December = {"", ""};
public static readonly string[] Monday = {"", ""};
public static readonly string[] Tuesday = {"", ""};
public static readonly string[] Wednesday = {"", ""};
public static readonly string[] Thursday = {"", ""};
public static readonly string[] Friday = {"", ""};
public static readonly string[] Saturday = {"", ""};
public static readonly string[] Sunday = {"", ""};
public static readonly string[] DaytimeDay = {"", ""};
public static readonly string[] TimeFrom = {"", ""};
public static readonly string[] TimeTo = {"", ""};
public static readonly string[] TimeOn = {""};
public static List<string[]> Months()
{
return new List<string[]>
{
January,
February,
March,
April,
May,
June,
July,
August,
September,
October,
November,
December
};
}
public static List<string[]> DaysOfWeek()
{
return new List<string[]>
{
Monday,
Tuesday,
Wednesday,
Thursday,
Friday,
Saturday,
Sunday
};
}
public List<string> AllValues()
{
var values = new List<string>();
GetType()
.GetFields(BindingFlags.Static | BindingFlags.Public)
.ToList()
.ForEach(f =>
{
var words = (string[]) f.GetValue(null);
words.ToList().ForEach(values.Add);
});
return values;
}
}
view raw Keywords.cs hosted with ❤ by GitHub

这就是我们提到的代码行:


弦乐结果
我明天去散步5__
明天晚上我去散散步5v__
下星期四我去看电影fxD_f_
下周四晚上9点我去电影院fxDf0v_f_
3月21日上午10点开会0Mf0r_

认可度


-恐怖! -你说-只会变得更糟! 这样的胡言乱语和一个人无法理解。 是的,我不得不在一个人阅读的便利和与常客打交道的便利之间做出一些折衷,具体请参见下文。


然后,我们采用一种称为职责链的模式:将输入数据数组顺序地馈送到不同的处理器,处理器可能会更改也可能不会更改它,并进一步传输它。 我给处理程序Recognizer打电话,并为与日期和时间有关的典型短语(在我看来)的每个变Recognizer作了这样一个处理程序。


Recognizer在字符串中搜索给定的正则表达式模式,并为找到的每个匹配项启动处理功能,该功能可以修改输入字符串并将捕获的日期添加到特殊数组中。


识别器
public abstract class Recognizer
{
public void ParseTokens(DatesRawData data, DateTime userDate)
{
ForAllMatches(data.GetPattern, pattern: GetRegexPattern(), action: m => ParseMatch(data, m, userDate));
}
public static void ForAllMatches(Func<string> input, string pattern, Predicate<Match> action, bool reversed = false)
{
var matches = Regex.Matches(input.Invoke(), pattern);
if (matches.Count == 0)
{
return;
}
var match = reversed ? matches[matches.Count - 1] : matches[0];
var indexesToSkip = new HashSet<int>();
while (match != null && match.Success)
{
var text = input.Invoke();
var matchIndex = reversed ? text.Length - match.Index : match.Index;
if (!action.Invoke(match))
{
indexesToSkip.Add(matchIndex);
}
match = null;
text = input.Invoke();
matches = Regex.Matches(text, pattern);
for (var i = 0; i < matches.Count; i++)
{
var index = reversed ? matches.Count - i - 1 : i;
matchIndex = reversed ? text.Length - matches[index].Index : matches[index].Index;
if (!indexesToSkip.Contains(matchIndex))
{
match = matches[index];
break;
}
}
}
}
protected abstract string GetRegexPattern();
protected abstract bool ParseMatch(DatesRawData data, Match match, DateTime userDate);
}
view raw Recognizer.cs hosted with ❤ by GitHub

而且,必须以正确的顺序调用处理程序:从更多“有信心”到更少。 例如,从一开始,您就需要运行与“严格”单词相关的处理程序,这些单词与日期完全相关:天,月,诸如“明天”,“明天之后”之类的单词,等等。 最后,处理人员尝试通过原始余额确定是否还有其他与日期相关的内容。


例如:


日期和月份


 "((0N?)+)(M|#)"; // 24, 25, 26...  27 / 

在这里,使用正则表达式的魅力开始显现。 用一种相当简单的方式,我们确定了一个复杂的令牌序列:一个非零数量的小于1901的非负数彼此紧随其后,并可能用联合“和”分隔,并且月份名称或单词“数字”紧随其后。


此外,在匹配组中,我们会立即捕获特定的元素(如果有),而不会捕获没有的元素,并根据该集合构成最终日期。 在下面的代码中,将有一个与FixFixPeriod函数相关的难以理解的部分,我们将在本文结尾处继续进行介绍。


DaysMonthRecognizer.cs
public class DaysMonthRecognizer : Recognizer
{
protected override string GetRegexPattern()
{
return "((0N?)+)(M|#)"; // 24, 25, 26... 27 /
}
protected override bool ParseMatch(DatesRawData data, Match match, DateTime userDate)
{
var dates = new List<AbstractPeriod>();
var monthFixed = false;
// parse month
var mStr = data.Tokens[match.Index + match.Groups[1].Length].Value;
var month = ParserUtils.FindIndex(mStr, Keywords.Months()) + 1;
if (month == 0) month = userDate.Month; // # instead M
else monthFixed = true;
// create dates
for (var i = match.Index; i < match.Index + match.Groups[1].Length; i++)
{
var t = data.Tokens[i];
int.TryParse(t.Value, out var day);
if (day <= 0) continue; // this is "AND" or other token
// current token is number, store it as a day
var period = new AbstractPeriod
{
Date = new DateTime(
userDate.Year,
month,
ParserUtils.GetDayValidForMonth(userDate.Year, month, day)
)
};
// fix from week to day, and year/month if it was
period.Fix(FixPeriod.Week, FixPeriod.Day);
if (monthFixed) period.Fix(FixPeriod.Month);
// store
dates.Add(period);
// compare with last if month not fixed
if (!monthFixed && dates.Count > 0 && dates.Last().Date < period.Date)
{
period.Date = new DateTime(
userDate.Year,
month + 1,
ParserUtils.GetDayValidForMonth(userDate.Year, month + 1, day)
);
}
}
// replace all scanned tokens
data.ReplaceTokensByDates(match.Index, match.Length, dates.ToArray());
return true;
}
}

时间流逝


 "(i)?((0?[Ymwdhe]N?)+)([bl])?"; // ()     2  4  10  (/) 

重要的是要注意,有时仅与常规季节相吻合的事实是不够的,您仍然需要向处理程序添加一些逻辑。 尽管可以为这种情况创建两个单独的处理程序,但是在我看来,这似乎有些多余。 结果,我同时匹配了初始的“通过”和最终的“以后/向后”,但是处理程序代码以检查开头:


 if (match.Groups[1].Success ^ match.Groups[4].Success) 

其中^是异或。


TimeSpanRecognizer.cs
public class TimeSpanRecognizer : Recognizer
{
protected override string GetRegexPattern()
{
return "(i)?((0?[Ymwdhe]N?)+)([bl])?"; // () 2 4 10 (/)
}
protected override bool ParseMatch(DatesRawData data, Match match, DateTime userDate)
{
if (match.Groups[1].Success ^ match.Groups[4].Success)
{
// if "after" of "before", but not both and not neither
var letters = match.Groups[2].Value.Select(s => s.ToString()).ToList();
var lastNumber = 1;
var tokenIndex = match.Groups[2].Index;
var direction = 1; // moving to the future
if (match.Groups[4].Success && match.Groups[4].Value == "b")
{
direction = -1; // "before"
}
var date = new AbstractPeriod
{
SpanDirection = direction,
};
// save current day to offser object
var offset = new DateTimeOffset(userDate);
letters.ForEach(l =>
{
switch (l)
{
case "N": // "and", skip it
break;
case "0": // number, store it
int.TryParse(data.Tokens[tokenIndex].Value, out lastNumber);
break;
case "Y": // year(s)
offset = offset.AddYears(direction * lastNumber);
date.FixDownTo(FixPeriod.Month);
lastNumber = 1;
break;
case "m": // month(s)
offset = offset.AddMonths(direction * lastNumber);
date.FixDownTo(FixPeriod.Week);
lastNumber = 1;
break;
case "w": // week(s)
offset = offset.AddDays(7 * direction * lastNumber);
date.FixDownTo(FixPeriod.Day);
lastNumber = 1;
break;
case "d": // day(s)
offset = offset.AddDays(direction * lastNumber);
date.FixDownTo(FixPeriod.Day);
lastNumber = 1;
break;
case "h": // hour(s)
offset = offset.AddHours(direction * lastNumber);
date.FixDownTo(FixPeriod.Time);
lastNumber = 1;
break;
case "e": // minute(s)
offset = offset.AddMinutes(direction * lastNumber);
date.FixDownTo(FixPeriod.Time);
break;
}
tokenIndex++;
});
// set date
date.Date = new DateTime(offset.DateTime.Year, offset.DateTime.Month, offset.DateTime.Day);
if (date.IsFixed(FixPeriod.Time))
date.Time = new TimeSpan(offset.DateTime.Hour, offset.DateTime.Minute, 0);
date.Span = offset - userDate;
// remove and insert
data.ReplaceTokensByDates(match.Index, match.Length, date);
return true;
}
return false;
}
}

顺便说一句,为了使这些功能正常工作,引擎需要转移用户的当前日期。


年份


 "(1)Y?|(0)Y"; // [] 15 /2017 () 

我不能再一次注意到常规人员使用该方法的便利性。 同样,我们使用一个简单的表达式立即指示了一些选项:用户使用与年份相似的数字命名,但不包含单词“ year”,或者用户使用两位数的数字(可以是日期),但添加单词“ year”,并支持“ 18年,“但是,我们不知道这是1918年还是2018年,因此我们相信2018年。


YearRecognizer.cs
public class YearRecognizer : Recognizer
{
protected override string GetRegexPattern()
{
return "(1)Y?|(0)Y"; // [] 15 /2017 ()
}
protected override bool ParseMatch(DatesRawData data, Match match, DateTime userDate)
{
// just year number
int.TryParse(data.Tokens[match.Index].Value, out var n);
var year = ParserUtils.GetYearFromNumber(n);
// insert date
var date = new AbstractPeriod
{
Date = new DateTime(year, 1, 1)
};
date.Fix(FixPeriod.Year);
// remove and insert
data.ReplaceTokensByDates(match.Index, match.Length, date);
return true;
}
}
view raw YearRecognizer.cs hosted with ❤ by GitHub

时间


 "([rvgd])?([fot])?(Q|H)?(h|(0)(h)?)((0)e?)?([rvgd])?"; // (//) (/) /9 () (30 ()) (///) 

我的收藏中最困难的表达方式是用来表示一天中的时间的短语,从严格的上午9.30四分之一到晚上11点。


TimeRecognizer.cs
public class TimeRecognizer : Recognizer
{
protected override string GetRegexPattern()
{
return "([rvgd])?([fot])?(Q|H)?(h|(0)(h)?)((0)e?)?([rvgd])?"; // (//) (/) /9 () (30 ()) (///)
}
protected override bool ParseMatch(DatesRawData data, Match match, DateTime userDate)
{
// determine if it is time
if (
match.Groups[5].Success //
|| match.Groups[6].Success // ""
|| match.Groups[4].Success // ""
|| match.Groups[1].Success // "///"
|| match.Groups[9].Success //
)
{
if (!match.Groups[5].Success)
{
// no number in phrase
var partOfDay = match.Groups[9].Success
? match.Groups[9].Value
: match.Groups[1].Success
? match.Groups[1].Value
: "";
// no part of day AND no "from" token in phrase, quit
if (partOfDay != "d" && partOfDay != "g" && !match.Groups[2].Success)
{
return false;
}
}
// hours and minutes
var hours = match.Groups[5].Success ? int.Parse(data.Tokens[match.Groups[5].Index].Value) : 1;
if (hours >= 0 && hours <= 23)
{
// try minutes
var minutes = 0;
if (match.Groups[8].Success)
{
var m = int.Parse(data.Tokens[match.Groups[8].Index].Value);
if (m >= 0 && m <= 59) minutes = m;
}
else if (match.Groups[3].Success && hours > 0)
{
switch (match.Groups[3].Value)
{
case "Q": // quarter
hours--;
minutes = 15;
break;
case "H": // half
hours--;
minutes = 30;
break;
}
}
// create time
var date = new AbstractPeriod();
date.Fix(FixPeriod.TimeUncertain);
if (hours > 12) date.Fix(FixPeriod.Time);
// correct time
if (hours <= 12)
{
var part = "d"; // default
if (match.Groups[9].Success || match.Groups[1].Success)
{
// part of day
part = match.Groups[1].Success ? match.Groups[1].Value : match.Groups[9].Value;
date.Fix(FixPeriod.Time);
}
else
{
date.Fix(FixPeriod.TimeUncertain);
}
switch (part)
{
case "d": // day
if (hours <= 4) hours += 12;
break;
case "v": // evening
if (hours <= 11) hours += 12;
break;
case "g": // night
hours += 12;
break;
}
if (hours == 24) hours = 0;
}
date.Time = new TimeSpan(hours, minutes, 0);
// remove and insert
var toTime = data.Tokens[match.Index];
data.ReplaceTokensByDates(match.Index, match.Length, date);
if (match.Groups[2].Success && match.Groups[2].Value == "t")
{
// return "to" to correct period parsing
data.ReturnTokens(match.Index, "t", toTime);
}
return true;
}
}
return false;
}
}
view raw TimeRecognizer.cs hosted with ❤ by GitHub

所有处理程序按申请顺序


该系统是模块化的,可以理解,您可以添加/删除处理程序,更改其顺序等。 我做了11个(按应用顺序从上到下):


处理程序正则表达式行示例
HolidaysRecognizer.cs 1W周末休息
DatesPeriodRecognizer.csf?(0)[ot]0(M|#)从1月26日到1月27日
DaysMonthRecognizer.cs((0N?)+)(M|#)1月24日,25日,26日和1月27日
MonthRecognizer.cs([usxy])?M[输入](过去/当前/下一个)三月
RelativeDayRecognizer.cs[2-6]前天,昨天,今天,明天,后天
TimeSpanRecognizer.cs(i)?((0?[Ymwdhe]N?)+)([bl])?(英寸)年,月和2天4小时10分钟(后/后)
YearRecognizer.cs(1)Y?|(0)Y[输入] 15年/ 2017(年)
RelativeDateRecognizer.cs([usxy])([Ymwd])[上/下]下一个/本/上一年/月/周/日
DayOfWeekRecognizer.cs([usxy])?(D)[输入](下一个/下一个/上一个)星期一
TimeRecognizer.cs([rvgd])?([fot])?(Q|H)?(h|(0)(h)?)((0)e?)?([rvgd])?(w / s / up)(半/季度)小时/ 9(小时)(30(分钟))(早晨/白天/晚上/晚上)
PartOfDayRecognizer.cs(@)?f?([ravgdn])f?(@)?(日期)(w / s)上午/下午/晚上/晚上(w / s)(日期)

1 "" "" "" " ",


一起缝


好的,我们定义了一些基本的日期时间标记。 但是现在我们仍然需要以某种方式考虑所有类型令牌的所有组合:用户可以先命名日期,然后再命名时间。 或命名月份和日期。 如果他只说时间,那么大概是今天,依此类推。 为此,我们提出了固定的概念。


在我们的案例中,一个解决方法是在每个日期时间标记内添加一个位掩码 ,以显示其中设置了哪些日期和时间元素。


 public enum FixPeriod { None = 0, Time = 1, TimeUncertain = 2, Day = 4, Week = 8, Month = 16, Year = 32 } 

TimeUncertain是一种承诺,用于指示可以进行细化的时间,例如晚上/早晨 。 不能说是早上18点 ,而可以说是晚上6点 ,因此数字6含义比“数字18


词组固定面膜
三月26,2019den ,vrm1,vrm2111100
26个数字年,月,周, den ,vrm1,vrm2000100
星期一年,月,周, den ,vrm1,vrm2000100
下周一den ,vrm1,vrm2111100
下个礼拜 ,den,vrm1,vrm2111000
在9点年,月,周,den, vrm1 ,vrm2000010
晚上9点年,月,周,den, vrm1vrm2000011

接下来,我们将使用两个规则:


  • 如果日期临近,并且没有通用位,那么我们将它们合并为一个
  • 如果日期临近,并且我们没有将它们组合在一起,但是其中一个具有第二个所没有的位,那么我们从它们所在的位置到没有它们的位置占据这些位(以及与它们相对应的值),但仅限于更大范围:也就是说,如果只给出一个月,我们就不能占用一天,但是如果只设置了一天,我们就可以占用一个月。


因此,如果用户在星期一晚上9点讲话,则将星期一 (仅设置日期)的令牌与令牌( 在晚上9点 )设置时间的令牌相结合。 但是,如果他在3月10日和15日说 ,那么一天15中的最后一个令牌将仅取前一个令牌中的3月份


工会是微不足道的,但是只要借钱,我们就能简单地进行下去。 我们将称其为基准日期,并称其为第二个日期,我们想从该日期借些东西。 然后,我们复制次要日期,仅保留基本日期没有的那些位:


 var baseDate = data.Dates[firstIndex]; var secondDate = data.Dates[secondIndex]; var secondCopy = secondDate.CopyOf(); secondCopy.Fixed &= (byte)~baseDate.Fixed; 

例如,如果基地有一年,我们将不保留未成年人副本的年份固定(即使是)。 如果基准站没有一个月,但是辅助站点有一个月,它将与副本一起保留为辅助站点。 之后,我们将合并基准日期和辅助副本。


当合并时,我们还有一个基准日期和一个吸收日期。 我们从最大的时期(年份)到最小的时期(时间)从上到下。 如果基准日期不具有吸收对象的某些参数,则将其添加到吸收对象的基础。


Collapse.cs
public static bool CanCollapse(AbstractPeriod basePeriod, AbstractPeriod coverPeriod)
{
if ((basePeriod.Fixed & coverPeriod.Fixed) != 0) return false;
return basePeriod.SpanDirection != -coverPeriod.SpanDirection || basePeriod.SpanDirection == 0;
}
public static bool CollapseTwo(AbstractPeriod basePeriod, AbstractPeriod coverPeriod)
{
if (!CanCollapse(basePeriod, coverPeriod)) return false;
// if span
if (basePeriod.SpanDirection != 0)
{
if (coverPeriod.SpanDirection != 0)
{
// if another date is Span, just add spans together
basePeriod.Span += coverPeriod.Span;
}
}
// take year if it is not here, but is in other date
if (!basePeriod.IsFixed(FixPeriod.Year) && coverPeriod.IsFixed(FixPeriod.Year))
{
basePeriod.Date = new DateTime(coverPeriod.Date.Year, basePeriod.Date.Month, basePeriod.Date.Day);
basePeriod.Fix(FixPeriod.Year);
}
// take month if it is not here, but is in other date
if (!basePeriod.IsFixed(FixPeriod.Month) && coverPeriod.IsFixed(FixPeriod.Month))
{
basePeriod.Date = new DateTime(basePeriod.Date.Year, coverPeriod.Date.Month, basePeriod.Date.Day);
basePeriod.Fix(FixPeriod.Month);
}
// week and day
if (!basePeriod.IsFixed(FixPeriod.Week) && coverPeriod.IsFixed(FixPeriod.Week))
{
// the week is in another date, check where is a day
if (basePeriod.IsFixed(FixPeriod.Day))
{
// set day of week, take date
basePeriod.Date = TakeDayOfWeekFrom(coverPeriod.Date, basePeriod.Date);
basePeriod.Fix(FixPeriod.Week);
}
else if (!coverPeriod.IsFixed(FixPeriod.Day))
{
// only week here, take it by taking a day
basePeriod.Date = new DateTime(basePeriod.Date.Year, basePeriod.Date.Month, coverPeriod.Date.Day);
basePeriod.Fix(FixPeriod.Week);
}
}
else if (basePeriod.IsFixed(FixPeriod.Week) && coverPeriod.IsFixed(FixPeriod.Day))
{
// here is a week, but day of week in other date
basePeriod.Date = TakeDayOfWeekFrom(basePeriod.Date, coverPeriod.Date);
basePeriod.Fix(FixPeriod.Week, FixPeriod.Day);
}
// day
if (!basePeriod.IsFixed(FixPeriod.Day) && coverPeriod.IsFixed(FixPeriod.Day))
{
if (coverPeriod.FixDayOfWeek)
{
// take only day of week from cover
basePeriod.Date = TakeDayOfWeekFrom(
new DateTime(
basePeriod.Date.Year, basePeriod.Date.Month,
basePeriod.IsFixed(FixPeriod.Week) ? basePeriod.Date.Day : 1
),
coverPeriod.Date,
!basePeriod.IsFixed(FixPeriod.Week)
);
basePeriod.Fix(FixPeriod.Week, FixPeriod.Day);
}
else
{
// take day from cover
basePeriod.Date = new DateTime(basePeriod.Date.Year, basePeriod.Date.Month, coverPeriod.Date.Day);
basePeriod.Fix(FixPeriod.Week, FixPeriod.Day);
}
}
// time
var timeGot = false;
if (!basePeriod.IsFixed(FixPeriod.Time) && coverPeriod.IsFixed(FixPeriod.Time))
{
basePeriod.Fix(FixPeriod.Time);
if (!basePeriod.IsFixed(FixPeriod.TimeUncertain))
{
basePeriod.Time = coverPeriod.Time;
}
else
{
if (basePeriod.Time.Hours <= 12 && coverPeriod.Time.Hours > 12)
{
basePeriod.Time += new TimeSpan(12, 0, 0);
}
}
timeGot = true;
}
if (!basePeriod.IsFixed(FixPeriod.TimeUncertain) && coverPeriod.IsFixed(FixPeriod.TimeUncertain))
{
basePeriod.Fix(FixPeriod.TimeUncertain);
if (basePeriod.IsFixed(FixPeriod.Time))
{
// take time from cover, but day part from base
var offset = coverPeriod.Time.Hours <= 12 && basePeriod.Time.Hours > 12 ? 12 : 0;
basePeriod.Time = new TimeSpan(coverPeriod.Time.Hours + offset, coverPeriod.Time.Minutes, 0);
}
else
{
basePeriod.Time = coverPeriod.Time;
timeGot = true;
}
}
// if this date is Span and we just got time from another non-span date, add this time to Span
if (timeGot && basePeriod.SpanDirection != 0 && coverPeriod.SpanDirection == 0)
{
basePeriod.Span += basePeriod.SpanDirection == 1 ? basePeriod.Time : -basePeriod.Time;
}
// set tokens edges
basePeriod.Start = Math.Min(basePeriod.Start, coverPeriod.Start);
basePeriod.End = Math.Max(basePeriod.End, coverPeriod.End);
return true;
}
view raw Collapse.cs hosted with ❤ by GitHub

另外,您需要考虑几个细微差别:


  • 基准日期可能有固定的一天,但没有一周。 在这种情况下,您需要从吸收日期起算一周,但要从基础日期算起星期几。
  • 但是,如果吸收的日期没有固定的日期,而是固定的一周,则我们需要从中获取日期(即年+月+日期),然后以这种方式设置星期,因为DateTime对象中没有单独的实体“ Week”。
  • 如果将TimeUncertain固定为基准日期,并且将Time吸收到Time吸收的基准日期上,而Time吸收的基准的小时数大于12,而基数较少,则应该将12小时加到基数上。 因为晚上5点到17点之间您无法说。 如果“不确定”日期的时间紧接在其旁边,则“不确定”日期的时间不能从一天的一半开始。 人们不这么说。 如果我们说的是从凌晨5点到下午5点的短语,那么两个日期都有一个“确定的”时间,这没有问题。

合并后,如果某些日期为空,则将其替换为用户当前日期的值:例如,如果用户未命名年份,则说明当前年份,如果您未命名月份,则为当前月份,依此类推。 当然,可以将任何DateTime对象传递给“当前日期”参数以提高灵活性。


拐杖


并不是所有的图书馆时刻都经过了周详的考虑。 为了使其正常工作,添加了以下拐杖:


  • 日期分别以“ b \ s \ co”开头的令牌和日期以“ b \ s \ co”开头的令牌组合。 因为,如果用户命名了期间,则不可能从开始日期和结束日期开始组合令牌。
  • 我并没有开始为一周中的某天引入单独的注视级别,因为它仅在一个地方需要使用:明显由一个带有星期几名称的单词给出。 为此做了一个标志。
  • 单独运行会合并彼此相距一定距离的日期。 这由collapseDistance参数控制,默认情况下为4个标记。 例如,该短语将起作用: 后天,在12点与朋友见面 。 但这是行不通的: 后天,我将在12点与我挚爱的好朋友见面

总结



  • 您可以在项目中使用该 。 她已经可以应付许多选择,但我会对其进行完善和重构。 欢迎提出带有测试的请求。 通常,提出一个听起来很现实的测试(就像人们在生活中所说的那样),但它破坏了库。
  • .NET Fiddle上的实时演示也可以运行,尽管据称该代码带有错误下划线,但它可以启动。 在控制台的底部,您可以输入俄语的短语,不要忘记数字应该是数字。
  • 与控制台应用程序相同的演示

现场结果是这样的:


Source: https://habr.com/ru/post/zh-CN471204/


All Articles