
مهمة
مرحبا يا هبر! أصبحت مهتمة بمهارات أليس وبدأت في التفكير في الفوائد التي يمكن أن يحققوها. هناك العديد من الألعاب الرائعة المختلفة على الموقع (بما في ذلك ألعابي) ، لكنني أردت إنشاء أداة عمل مطلوبة حقًا في الأداء الصوتي ، وليس فقط نسخ برنامج الدردشة الموجود باستخدام أزرار.
يكون الصوت ذا صلة عندما تكون الأيدي مشغولة ، أو تحتاج إلى تنفيذ العديد من العمليات المتسلسلة ، خاصة على شاشة الهاتف. لذلك نشأت فكرة المهارة التي تقوم ، في أمر واحد ، باستخراج إشارة إلى التاريخ والوقت من النص وإضافة حدث بهذا النص إلى تقويم 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 صباحا
نرى أن الكلمات ككل تنقسم إلى ثلاثة أنواع:
- تلك التي تشير دائمًا إلى التواريخ والأوقات (أسماء الأشهر والأيام)
- تلك التي تتعلق بالتاريخ والوقت في موضع معين بالنسبة إلى الكلمات الأخرى ("اليوم" ، "المساء" ، "التالي" ، الأرقام)
- تلك التي لا تاريخ ووقت
مع الأول والأخير ، كل شيء واضح ، ولكن مع الثاني هناك صعوبات. كانت النسخة الأصلية من الخوارزمية عبارة عن كود إسباجيتي فظيع يحتوي على عدد كبير من if
، لأنني حاولت أن تأخذ في الاعتبار جميع المجموعات الممكنة والتباديل للكلمات التي أحتاجها ، ولكن بعد ذلك توصلت إلى خيار أفضل. والحقيقة هي أن الإنسانية قد اخترعت بالفعل نظامًا يتيح لك أن تأخذ في الاعتبار بسرعة وسهولة التباديل ومجموعات الأحرف: محرك التعبير العادي.
طبخ خط
نقوم بتقسيم الخط إلى رموز ، وإزالة علامات الترقيم وتقليل كل شيء إلى أحرف صغيرة. بعد ذلك ، نستبدل كل رمز غير فارغ بحرف واحد لتسهيل العمل مع التعبيرات العادية.
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; |
| } |
| } |
إليك ما يحدث للخطوط التي ذكرناها:
اعتراف
- أوه ، رعب! - أنت تقول ، - ازداد الأمر سوءًا! مثل هذا رطانة وشخص لا يمكن أن نفهم. نعم ، كان عليّ أن أقدم بعض التسوية بين راحة القراءة من قِبل شخص ما وراحة العمل مع النظامي ، انظر أدناه بالضبط كيف.
ثم نطبق نمطًا يسمى سلسلة المهام : يتم تغذية صفيف بيانات الإدخال بالتتابع على معالجات مختلفة ، والتي قد تغيرها أو لا تغيرها وتنقلها أكثر. لقد اتصلت بـ " 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); |
| } |
علاوة على ذلك ، يجب استدعاء المعالجات بالترتيب الصحيح: من أكثر "ثقة" إلى أقل. على سبيل المثال ، في البداية ، تحتاج إلى تشغيل معالجات مرتبطة بكلمات "صارمة" ترتبط تمامًا بالتواريخ: أسماء الأيام والشهور والكلمات مثل "غد" و "اليوم التالي غدًا" وما إلى ذلك. وفي النهاية ، يقوم القائمون على المعالجة الذين يحاولون تحديد أرصدة الخام بتحديد ما إذا كان هناك أي شيء آخر متعلق بالتاريخ.
على سبيل المثال:
التاريخ والشهر
"((0N?)+)(M|#)";
هنا يبدأ سحر استخدام التعبيرات العادية في الظهور. بطريقة بسيطة إلى حد ما ، حددنا تسلسلًا معقدًا من الرموز المميزة: يتبع عدد غير صفري من الأرقام غير السالبة أقل من 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])?";
من المهم أن نلاحظ أنه في بعض الأحيان لا تكون حقيقة المصادفة مع الموسم العادي كافية ، ولا تزال بحاجة إلى إضافة بعض المنطق إلى المعالج. رغم أنه كان من الممكن إنشاء معالجين منفصلين لمثل هذه الحالات ، لكن بدا لي أنه فائض. كنتيجة لذلك ، أقوم بمطابقة كلٍّ من "إلى" والنهائي "لاحقًا / للخلف" ، لكن رمز المعالج يبدأ بعملية تحقق:
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";
لا أستطيع مرة أخرى ملاحظة راحة النهج مع النظامي. مرة أخرى ، مع تعبير بسيط ، أشرنا على الفور إلى الخيارات: يسمي المستخدم رقمًا مشابهًا لسنة ، ولكن بدون كلمة "سنة" ، أو يقوم المستخدم بتسمية رقم مكون من خانتين (والذي يمكن أن يكون تاريخًا) ، لكننا نضيف كلمة "سنة" ، ودعم التعبيرات "في 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; |
| } |
| } |
وقت
"([rvgd])?([fot])?(Q|H)?(h|(0)(h)?)((0)e?)?([rvgd])?";
أصعب عبارة في مجموعتي هي المسؤولة عن العبارات التي تشير إلى الوقت من اليوم ، من الساعة 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; |
| } |
| } |
جميع معالجات في ترتيب التطبيق
النظام معياري ، من المفهوم أنه يمكنك إضافة / إزالة معالجات وتغيير ترتيبها وما إلى ذلك. لقد صنعت 11 من هذه (من أعلى إلى أسفل في ترتيب الطلب):
1 "" "" "" " ",
خياطة معا
حسنًا ، لقد حددنا بعض الرموز المميزة لوقت التاريخ. ولكن الآن ما زلنا بحاجة إلى أن نأخذ في الاعتبار بطريقة ما كل المجموعات من جميع أنواع الرموز: يمكن للمستخدم تسمية اليوم ، ثم الوقت. أو اسم الشهر واليوم. إذا قال الوقت فقط ، فمن المحتمل أن يكون الأمر اليوم ، وهكذا. للقيام بذلك ، توصلنا إلى مفهوم التثبيت .
الإصلاح في حالتنا عبارة عن ماسك ضوئي داخل كل رمز مميز للتاريخ ، والذي يوضح عناصر التاريخ والوقت التي تم تعيينها فيه.
public enum FixPeriod { None = 0, Time = 1, TimeUncertain = 2, Day = 4, Week = 8, Month = 16, Year = 32 }
TimeUncertain
هو التزام TimeUncertain
إلى الوقت الذي يمكن أن يتبعه التنقيح ، على سبيل المثال ، في المساء / في الصباح . لا يمكن قول الساعة 18 صباحًا ، ولكن يمكن قول الساعة 6 مساءً ، وبالتالي فإن الرقم 6
لديه ، كما كان ، أقل "يقينًا" بشأن الوقت الذي يعنيه من الرقم 18
.
بعد ذلك ، سوف نستخدم قاعدتين:
- إذا كانت التواريخ قريبة ولا تحتوي على وحدات بت مشتركة ، فسنقوم بدمجها في واحد
- إذا كانت التواريخ قريبة ، ولم نجمعها ، لكن أحدها يحتوي على بتات ليس لها الثانية ، فإننا نحتل البتات (والقيم المقابلة لها) من تلك الموجودة فيها ، إلى تلك التي لا توجد فيها ، ولكن فقط للأكبر: أي أنه لا يمكننا أن نأخذ يومًا إذا تم إعطاء شهر واحد فقط ، لكن يمكننا أن نأخذ شهرًا إذا تم تحديد يوم واحد فقط.

وبالتالي ، إذا كان المستخدم يتحدث يوم الاثنين الساعة 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; |
| } |
بشكل منفصل ، عليك أن تأخذ في الاعتبار بضع الفروق الدقيقة:
- قد يكون تاريخ الأساس يومًا ثابتًا ، ولكن ليس أسبوعًا. في هذه الحالة ، تحتاج إلى أن تأخذ أسبوعًا من تاريخ الامتصاص ، ولكن في يوم الأسبوع من القاعدة.
- ومع ذلك ، إذا لم يكن التاريخ الذي تمتصه يومًا محددًا ، ولكن تم إصلاح الأسبوع ، فسنحتاج إلى أخذ اليوم منه (أي ، السنة + الشهر + التاريخ) وتعيين الأسبوع بهذه الطريقة ، لأنه لا يوجد كيان منفصل "أسبوع" في كائن
DateTime
. - إذا
TimeUncertain
تثبيت TimeUncertain
في تاريخ الأساس ، وتم امتصاص Time
الذي تمتصه ، وكان عدد الساعات في واحدة تمتصه أكثر من 12 ، في حين أن القاعدة أقل ، فيجب إضافة 12 ساعة إلى القاعدة. لأنه لا يمكنك القول من 5 إلى 17 في المساء . لا يمكن أن يكون وقت التاريخ "غير المؤكد" من نصف اليوم ، إذا كان وقت التاريخ "الواثق" بجانبه هو من النصف الآخر من اليوم. الناس لا يقولون ذلك. إذا قلنا العبارة من 5 صباحًا إلى 5 مساءً ، فسيكون لكلا التاريخين وقت "واثق" ولا توجد مشكلة.
بعد الجمع ، إذا كانت بعض التواريخ تحتوي على وحدات بت فارغة ، فإننا نستبدلها بقيم من التاريخ الحالي للمستخدم: على سبيل المثال ، إذا لم يقم المستخدم بتسمية السنة ، فنحن نتحدث عن السنة الحالية ، وإذا لم تقم بتسمية الشهر ، ثم الشهر الحالي وما إلى ذلك. بالطبع ، يمكن تمرير أي كائن DateTime
إلى معلمة "التاريخ الحالي" للمرونة.
ركائز
لم يتم التفكير في جميع لحظات المكتبة بأمان. لجعلها تعمل بشكل صحيح ، تم إضافة العكازات التالية:
- يتم دمج التواريخ بشكل منفصل للرموز التي تبدأ بـ "b \ s \ co" وبشكل منفصل للرموز التي تبدأ بـ "b \ to \ on". لأنه إذا قام المستخدم بتسمية الفترة ، فمن المستحيل الجمع بين الرموز المميزة من تاريخ البدء وتاريخ الانتهاء.
- لم أبدأ في تقديم مستوى تثبيت منفصل ليوم الأسبوع ، لأنه مطلوب في مكان واحد بالضبط: حيث يتم تقديمه بوضوح بكلمة تحمل اسم يوم الأسبوع. صنع العلم لهذا.
- يجمع التشغيل المنفصل بين التواريخ التي تقع على مسافة معينة من بعضها البعض. يتم التحكم في هذا بواسطة المعلمة
collapseDistance
، والتي هي افتراضيًا 4 رموز. هذا ، على سبيل المثال ، ستعمل العبارة: يوم غد ، لقاء مع صديق في الثانية عشرة . لكن هذا لن ينجح: في اليوم التالي غداً ، سألتقي بصديقي المحبوب والرائع في الثانية عشرة .
يؤدي

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