التعرف على التاريخ والوقت في الكلام الطبيعي


مهمة


مرحبا يا هبر! أصبحت مهتمة بمهارات أليس وبدأت في التفكير في الفوائد التي يمكن أن يحققوها. هناك العديد من الألعاب الرائعة المختلفة على الموقع (بما في ذلك ألعابي) ، لكنني أردت إنشاء أداة عمل مطلوبة حقًا في الأداء الصوتي ، وليس فقط نسخ برنامج الدردشة الموجود باستخدام أزرار.


يكون الصوت ذا صلة عندما تكون الأيدي مشغولة ، أو تحتاج إلى تنفيذ العديد من العمليات المتسلسلة ، خاصة على شاشة الهاتف. لذلك نشأت فكرة المهارة التي تقوم ، في أمر واحد ، باستخراج إشارة إلى التاريخ والوقت من النص وإضافة حدث بهذا النص إلى تقويم Google . على سبيل المثال ، إذا قال أحد المستخدمين في اليوم التالي للغد في الساعة 11 مساءً ، فسوف يكون هناك غروب جميل ، عندها سيكون هناك غروب جميل في التقويم لليوم التالي للغد في الساعة 11:00 مساءً .


تحت القاطع هو وصف لخوارزمية مكتبة Hors : أداة التعرف على التاريخ والوقت في الكلام الروسي الطبيعي. الحصان هو إله الشمس السلافي.


جيثب | NuGet


الحلول الحالية


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 ') 

مزمن على روبي
مكتبة معروفة بالرووبيات ، والتي ، كما يقولون ، تؤدي وظيفتها على أكمل وجه. ولكن لم يتم العثور على دعم الروسية.


مساعد جوجل
بما أننا نتحدث عن إضافة صوت إلى تقويم Google ، فإننا نسأل ، لماذا لا نستخدم المساعد؟ من الممكن ، لكن فكرة المشروع هي القيام بالعمل في عبارة واحدة دون إيماءات وصنابير غير ضرورية. وللقيام بذلك بشكل موثوق. مساعد لديه مشاكل مع هذا الآن:



المسبقة


كتبت مكتبة على .NETStandard 2.0 (C #) . نظرًا لأنه تم إنشاء المكتبة في الأصل لـ Alice ، فمن المفترض أن تكون جميع الأرقام الموجودة في النص أرقامًا ، لأن Alice تقوم بهذا التحويل تلقائيًا. إذا كان لديك أرقام في السلاسل ، فهناك مقالة Doomer3D رائعة حول كيفية تحويل الكلمات إلى أرقام.


مورفولوجيا


عند العمل باستخدام إدخال الصوت ، فإن الطريقة الأكثر موثوقية لتمييز الكلمات الصحيحة عن الخطأ هي استخدام أشكال الكلمات. تحدثت أنا وصديق عن هذا بمزيد من التفصيل في فيديو تعليمي لمدرسة أليس من ياندكس. في هذه المقالة ، سأترك وراء الكواليس أعمل مع أشكال الكلمة. سوف تظهر هذه البنيات في الكود:


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

تُرجع هذه الدالة true إذا كانت الكلمة t عبارة عن أي شكل من أشكال الكلمات الثلاث التالية ، على سبيل المثال: الماضي ، الماضي ، السابق .


نظرية


لفهم كيفية التقاط التواريخ في النص ، تحتاج إلى سرد العبارات النموذجية التي نستخدمها للإشارة إلى التواريخ في محادثات حقيقية. على سبيل المثال:


- سأذهب للمشي غدا
- سأذهب للنزهة مساء الغد
- الخميس القادم سأذهب إلى السينما
- الخميس المقبل الساعة 9 مساءً أذهب إلى السينما
- 21 مارس في الساعة 10 صباحا


نرى أن الكلمات ككل تنقسم إلى ثلاثة أنواع:


  1. تلك التي تشير دائمًا إلى التواريخ والأوقات (أسماء الأشهر والأيام)
  2. تلك التي تتعلق بالتاريخ والوقت في موضع معين بالنسبة إلى الكلمات الأخرى ("اليوم" ، "المساء" ، "التالي" ، الأرقام)
  3. تلك التي لا تاريخ ووقت

مع الأول والأخير ، كل شيء واضح ، ولكن مع الثاني هناك صعوبات. كانت النسخة الأصلية من الخوارزمية عبارة عن كود إسباجيتي فظيع يحتوي على عدد كبير من if ، لأنني حاولت أن تأخذ في الاعتبار جميع المجموعات الممكنة والتباديل للكلمات التي أحتاجها ، ولكن بعد ذلك توصلت إلى خيار أفضل. والحقيقة هي أن الإنسانية قد اخترعت بالفعل نظامًا يتيح لك أن تأخذ في الاعتبار بسرعة وسهولة التباديل ومجموعات الأحرف: محرك التعبير العادي.


طبخ خط


نقوم بتقسيم الخط إلى رموز ، وإزالة علامات الترقيم وتقليل كل شيء إلى أحرف صغيرة. بعد ذلك ، نستبدل كل رمز غير فارغ بحرف واحد لتسهيل العمل مع التعبيرات العادية.


الرمز (كلمة)رمز ليحل محل
"السنة"Y
اسم الشهرM
اسم أيام الأسبوعD
"الخلف"ب
"في وقت لاحق"لتر (أقل لتر)
"من خلال"أنا
"يوم عطلة"W
"دقيقة"البريد
"ساعة"ح
"اليوم"د
"أسبوع"ث
"شهر"م
"الماضي" ، "الماضي" ، "السابق"الصورة
"هذا" ، "الحالي" ، "الحالي"ش
"الأقرب" ، "المستقبل"ذ
"التالي" ، "المستقبل"س
"بعد غد"6
"غدا"5
"اليوم"4
"بالأمس"3
"قبل يوم أمس"2
"الصباح"ص
"نون"ن
"المساء"الخامس
"ليلة"ز
"نصف"H
"ربع"Q
ج ، جو
"إلى" ، "بواسطة"تي
"على"حول
"الرقم"#
"و"N
عدد أكبر من 1900 وأقل من 99991
عدد غير سالب أقل من 19010
تاريخ معالجتها بالفعل من قبل الخوارزمية@
أي رمز آخر_

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;
}
}

Keywords.cs
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_
21 مارس في الساعة 10 صباحا0Mf0r_

اعتراف


- أوه ، رعب! - أنت تقول ، - ازداد الأمر سوءًا! مثل هذا رطانة وشخص لا يمكن أن نفهم. نعم ، كان عليّ أن أقدم بعض التسوية بين راحة القراءة من قِبل شخص ما وراحة العمل مع النظامي ، انظر أدناه بالضبط كيف.


ثم نطبق نمطًا يسمى سلسلة المهام : يتم تغذية صفيف بيانات الإدخال بالتتابع على معالجات مختلفة ، والتي قد تغيرها أو لا تغيرها وتنقلها أكثر. لقد اتصلت بـ " Recognizer على المعالج" وأدليت بمعالج لكل متغير عبارة (كما يبدو لي) عبارة تتعلق بالتاريخ والوقت.


يبحث Recognizer على نمط regex معين في سلسلة ويقوم بتشغيل وظيفة معالجة لكل مطابقة يتم العثور عليها والتي يمكن أن تعدّل سلسلة الإدخال وتضيف التواريخ التي تم ضبطها إلى صفيف خاص.


Recognizer.cs
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 بعضها البعض ، وربما تكون مفصولة بالاتحاد "و" ، ويتبعها إما اسم الشهر أو كلمة "الرقم".


علاوة على ذلك ، في مجموعة المطابقة ، نلاحظ على الفور عناصر محددة ، إن وجدت ، ولا نلاحظ وجود أي منها ، ومن هذه المجموعة نؤلف التاريخ النهائي. في الكود أدناه ، سيكون هناك قسم غير مفهوم يتعلق بوظائف Fix ، FixPeriod ، سننتقل إلى هذا في نهاية المقالة.


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) 

حيث ^ هو الحصري OR.


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 () 

لا أستطيع مرة أخرى ملاحظة راحة النهج مع النظامي. مرة أخرى ، مع تعبير بسيط ، أشرنا على الفور إلى الخيارات: يسمي المستخدم رقمًا مشابهًا لسنة ، ولكن بدون كلمة "سنة" ، أو يقوم المستخدم بتسمية رقم مكون من خانتين (والذي يمكن أن يكون تاريخًا) ، لكننا نضيف كلمة "سنة" ، ودعم التعبيرات "في 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 صباحًا إلى الساعة 10:00 صباحًا إلى الساعة 11:00 مساءً.


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|#)من 26 يناير إلى 27 يناير / تاريخ
DaysMonthRecognizer.cs((0N?)+)(M|#)24 و 25 و 26 يناير و 27 يناير / تاريخ
MonthRecognizer.cs([usxy])?M[في] (الماضي / هذا / القادم) مارس
RelativeDayRecognizer.cs[2-6]أول من أمس ، أمس ، اليوم ، غدًا ، بعد غد
TimeSpanRecognizer.cs(i)?((0?[Ymwdhe]N?)+)([bl])?(في) السنة والشهر ويومان 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])?(ث / ث / أعلى) (نصف / ربع) ساعة / 9 (ساعات) (30 (دقيقة)) (صباح / يوم / مساء / ليلة)
PartOfDayRecognizer.cs(@)?f?([ravgdn])f?(@)?(التاريخ) (ث / ث) الصباح / بعد الظهر / المساء / الليل (ث / ث) (التاريخ)

1 "" "" "" " ",


خياطة معا


حسنًا ، لقد حددنا بعض الرموز المميزة لوقت التاريخ. ولكن الآن ما زلنا بحاجة إلى أن نأخذ في الاعتبار بطريقة ما كل المجموعات من جميع أنواع الرموز: يمكن للمستخدم تسمية اليوم ، ثم الوقت. أو اسم الشهر واليوم. إذا قال الوقت فقط ، فمن المحتمل أن يكون الأمر اليوم ، وهكذا. للقيام بذلك ، توصلنا إلى مفهوم التثبيت .


الإصلاح في حالتنا عبارة عن ماسك ضوئي داخل كل رمز مميز للتاريخ ، والذي يوضح عناصر التاريخ والوقت التي تم تعيينها فيه.


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

TimeUncertain هو التزام TimeUncertain إلى الوقت الذي يمكن أن يتبعه التنقيح ، على سبيل المثال ، في المساء / في الصباح . لا يمكن قول الساعة 18 صباحًا ، ولكن يمكن قول الساعة 6 مساءً ، وبالتالي فإن الرقم 6 لديه ، كما كان ، أقل "يقينًا" بشأن الوقت الذي يعنيه من الرقم 18 .


العبارةتثبيتقناع
26 مارس 2019السنة ، الشهر ، الأسبوع ، den ، vrm1 ، vrm2111100
26 رقمالسنة ، الشهر ، الأسبوع ، den ، vrm1 ، vrm2000100
الإثنينالسنة ، الشهر ، الأسبوع ، den ، vrm1 ، vrm2000100
الاثنين المقبلالسنة ، الشهر ، الأسبوع ، den ، vrm1 ، vrm2111100
الاسبوع المقبلالسنة ، الشهر ، الأسبوع ، den ، vrm1 ، vrm2111000
في الساعة 9السنة ، الشهر ، الأسبوع ، den ، vrm1 ، vrm2000010
في 9 مساءًالسنة ، الشهر ، الأسبوع ، den ، vrm1 ، vrm2000011

بعد ذلك ، سوف نستخدم قاعدتين:


  • إذا كانت التواريخ قريبة ولا تحتوي على وحدات بت مشتركة ، فسنقوم بدمجها في واحد
  • إذا كانت التواريخ قريبة ، ولم نجمعها ، لكن أحدها يحتوي على بتات ليس لها الثانية ، فإننا نحتل البتات (والقيم المقابلة لها) من تلك الموجودة فيها ، إلى تلك التي لا توجد فيها ، ولكن فقط للأكبر: أي أنه لا يمكننا أن نأخذ يومًا إذا تم إعطاء شهر واحد فقط ، لكن يمكننا أن نأخذ شهرًا إذا تم تحديد يوم واحد فقط.


وبالتالي ، إذا كان المستخدم يتحدث يوم الاثنين الساعة 9 مساءً ، فإن الرمز المميز يوم الاثنين ، حيث يتم تعيين اليوم فقط ، يتم دمجه مع الرمز في الساعة 9 مساءً ، حيث يتم ضبط الوقت. ولكن إذا قال في 10 و 15 مارس من اليوم ، فإن الرمز المميز الأخير في 15 من اليوم سيستغرق ببساطة شهر مارس من الرمز السابق.


الاتحاد تافه ، ولكن مع الاقتراض سنمضي ببساطة. سوف نسمي التاريخ الأساسي الذي نشغله ، والثاني الثانوي ، الذي نريد استعارة شيء منه. ثم ننسخ التاريخ الثانوي ونترك فقط تلك البتات التي لا تحتوي عليها القاعدة:


 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 .
  • إذا TimeUncertain تثبيت TimeUncertain في تاريخ الأساس ، وتم امتصاص Time الذي تمتصه ، وكان عدد الساعات في واحدة تمتصه أكثر من 12 ، في حين أن القاعدة أقل ، فيجب إضافة 12 ساعة إلى القاعدة. لأنه لا يمكنك القول من 5 إلى 17 في المساء . لا يمكن أن يكون وقت التاريخ "غير المؤكد" من نصف اليوم ، إذا كان وقت التاريخ "الواثق" بجانبه هو من النصف الآخر من اليوم. الناس لا يقولون ذلك. إذا قلنا العبارة من 5 صباحًا إلى 5 مساءً ، فسيكون لكلا التاريخين وقت "واثق" ولا توجد مشكلة.

بعد الجمع ، إذا كانت بعض التواريخ تحتوي على وحدات بت فارغة ، فإننا نستبدلها بقيم من التاريخ الحالي للمستخدم: على سبيل المثال ، إذا لم يقم المستخدم بتسمية السنة ، فنحن نتحدث عن السنة الحالية ، وإذا لم تقم بتسمية الشهر ، ثم الشهر الحالي وما إلى ذلك. بالطبع ، يمكن تمرير أي كائن DateTime إلى معلمة "التاريخ الحالي" للمرونة.


ركائز


لم يتم التفكير في جميع لحظات المكتبة بأمان. لجعلها تعمل بشكل صحيح ، تم إضافة العكازات التالية:


  • يتم دمج التواريخ بشكل منفصل للرموز التي تبدأ بـ "b \ s \ co" وبشكل منفصل للرموز التي تبدأ بـ "b \ to \ on". لأنه إذا قام المستخدم بتسمية الفترة ، فمن المستحيل الجمع بين الرموز المميزة من تاريخ البدء وتاريخ الانتهاء.
  • لم أبدأ في تقديم مستوى تثبيت منفصل ليوم الأسبوع ، لأنه مطلوب في مكان واحد بالضبط: حيث يتم تقديمه بوضوح بكلمة تحمل اسم يوم الأسبوع. صنع العلم لهذا.
  • يجمع التشغيل المنفصل بين التواريخ التي تقع على مسافة معينة من بعضها البعض. يتم التحكم في هذا بواسطة المعلمة collapseDistance ، والتي هي افتراضيًا 4 رموز. هذا ، على سبيل المثال ، ستعمل العبارة: يوم غد ، لقاء مع صديق في الثانية عشرة . لكن هذا لن ينجح: في اليوم التالي غداً ، سألتقي بصديقي المحبوب والرائع في الثانية عشرة .

يؤدي



  • يمكنك استخدام المكتبة في مشاريعك. لقد تعاملت بالفعل مع العديد من الخيارات ، لكنني قمت بصقلها وإعادة تشكيلها. سحب الطلبات مع الاختبارات هي موضع ترحيب. بشكل عام ، توصل إلى اختبار يبدو واقعيًا (كما يقول الناس في الحياة) ، لكنه يكسر المكتبة.
  • يعمل العرض التوضيحي المباشر على .NET Fiddle أيضًا ، على الرغم من أن التعليمات البرمجية مدعومة بالأخطاء ، لكنها تبدأ. في الجزء السفلي من وحدة التحكم ، يمكنك إدخال العبارات باللغة الروسية ، دون أن ننسى أن الأرقام يجب أن تكون أرقامًا.
  • نفس العرض التوضيحي كتطبيق وحدة التحكم

عيشها اتضح مثل هذا:


Source: https://habr.com/ru/post/ar471204/


All Articles