
Tantangan
Halo, Habr! Saya menjadi tertarik pada keterampilan untuk Alice dan mulai berpikir manfaat apa yang bisa mereka bawa. Ada banyak permainan keren yang berbeda di situs ini (termasuk milik saya), tetapi saya ingin membuat alat kerja yang benar-benar dibutuhkan dalam eksekusi suara, dan tidak hanya menyalin chatbot yang ada dengan tombol.
Suara itu relevan ketika tangan sibuk, atau Anda perlu melakukan banyak operasi sekuensial, terutama pada layar ponsel. Maka muncul gagasan keterampilan, yang, dengan satu perintah, mengekstraksi indikasi tanggal dan waktu dari teks dan menambahkan acara dengan teks ini ke Kalender Google . Misalnya, jika pengguna mengatakan lusa pukul 11 ββmalam akan ada matahari terbenam yang indah , maka garis akan ada matahari terbenam yang indah di kalender untuk lusa pukul 23:00.
Di bawah pemotong adalah deskripsi algoritma perpustakaan Hors : pengenal tanggal dan waktu dalam pidato Rusia alami. Kuda adalah dewa matahari Slavia.
Github | Nuget
Solusi yang ada
dateparser dengan Python
Dukungan untuk bahasa Rusia dinyatakan, tetapi dalam bahasa Rusia perpustakaan bahkan tidak dapat mengatasi hal-hal dasar:
>>> 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 ')
kronis di Ruby
Sebuah perpustakaan yang dikenal oleh para rubis, yang, menurut mereka, melakukan tugasnya dengan sempurna. Tetapi dukungan untuk Rusia tidak ditemukan.
Asisten Google
Karena kita berbicara tentang menambahkan suara ke Kalender Google, kami bertanya, mengapa tidak menggunakan Asisten? Itu mungkin, tetapi gagasan proyek ini adalah melakukan pekerjaan dalam satu frasa tanpa gerakan dan ketukan yang tidak perlu. Dan melakukannya dengan andal. Asisten memiliki masalah dengan ini untuk saat ini:

Preset
Saya menulis perpustakaan di .NETStandard 2.0 (C #) . Karena perpustakaan awalnya dibuat untuk Alice, semua angka dalam teks seharusnya berupa angka, karena Alice melakukan konversi ini secara otomatis. Jika Anda memiliki angka dalam string, maka ada artikel Doomer3D yang luar biasa tentang cara mengubah kata menjadi angka.
Morfologi
Ketika bekerja dengan input suara, cara yang paling dapat diandalkan untuk membedakan kata yang benar dari yang salah adalah dengan menggunakan bentuk kata. Seorang teman dan saya membicarakan hal ini lebih detail dalam tutorial video untuk Sekolah Alice dari Yandex. Pada artikel ini, saya akan meninggalkan pekerjaan layar dengan bentuk kata. Konstruksi seperti itu akan muncul dalam kode:
Morph.HasOneOfLemmas(t, "", "", "");
Fungsi ini mengembalikan true
jika kata t
adalah salah satu dari tiga kata berikut, misalnya: lampau , lampau , sebelumnya .
Teori
Untuk memahami cara menangkap tanggal dalam teks, Anda perlu membuat daftar frasa khas yang kami gunakan untuk menunjukkan tanggal dalam percakapan nyata. Sebagai contoh:
- Aku akan jalan-jalan besok
- Aku akan jalan-jalan besok malam
- Kamis depan saya pergi ke bioskop
- Kamis depan jam 9 malam saya pergi ke bioskop
- Pertemuan 21 Maret pukul 10 pagi
Kita melihat bahwa kata-kata secara keseluruhan dibagi menjadi tiga jenis:
- Yang selalu merujuk pada tanggal dan waktu (nama bulan dan hari)
- Kata-kata yang berhubungan dengan tanggal dan waktu pada posisi tertentu relatif terhadap kata-kata lain ("hari", "malam", "berikutnya", angka)
- Mereka yang tidak pernah berkencan dan waktu
Dengan yang pertama dan terakhir, semuanya jelas, tetapi dengan yang kedua ada kesulitan. Versi asli dari algoritma ini adalah kode spaghetti yang mengerikan dengan sejumlah besar if
, karena saya mencoba memperhitungkan semua kemungkinan kombinasi dan permutasi dari kata-kata yang saya butuhkan, tetapi kemudian saya menemukan pilihan yang lebih baik. Faktanya adalah bahwa manusia telah menemukan sebuah sistem yang memungkinkan Anda untuk dengan cepat dan mudah memperhitungkan permutasi akun dan kombinasi karakter: mesin ekspresi reguler.
Memasak garis
Kami memecah garis menjadi token, menghapus tanda baca dan mengurangi semuanya menjadi huruf kecil. Setelah itu, kami mengganti setiap token yang tidak kosong dengan satu karakter untuk membuatnya lebih mudah untuk bekerja dengan ekspresi reguler.
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; |
| } |
| } |
Inilah yang terjadi untuk baris yang kami sebutkan:
Pengakuan
- Oh, ngeri! - Anda berkata, - Itu hanya menjadi lebih buruk! Omong kosong dan seseorang tidak bisa mengerti. Ya, saya harus membuat beberapa kompromi antara kenyamanan membaca oleh seseorang dan kenyamanan bekerja dengan pelanggan tetap, lihat persis di bawah ini caranya.
Kemudian kami menerapkan pola yang disebut rantai tugas : array data input dimasukkan secara berurutan ke prosesor yang berbeda, yang mungkin atau mungkin tidak mengubahnya dan mengirimkannya lebih lanjut. Saya memanggil Handler Recognizer
dan membuat handler tersebut untuk setiap varian dari frasa khas (seperti yang bagi saya) berkaitan dengan tanggal dan waktu.
Recognizer
mencari pola regex yang diberikan dalam string dan meluncurkan fungsi pemrosesan untuk setiap kecocokan yang ditemukan yang dapat memodifikasi string input dan menambahkan tanggal yang tertangkap ke array khusus.
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); |
| } |
Selain itu, penangan harus dipanggil dalam urutan yang benar: dari yang lebih "percaya diri" hingga yang kurang. Misalnya, di awal, Anda harus menjalankan penangan yang terkait dengan kata-kata "ketat" yang persis berkaitan dengan tanggal: nama-nama hari, bulan, kata-kata seperti "besok", "lusa," dan seterusnya. Dan pada akhirnya, penangan yang mencoba menentukan dengan saldo mentah apakah ada hal lain yang terkait dengan tanggal tersebut.
Sebagai contoh:
Tanggal dan bulan
"((0N?)+)(M|#)";
Di sini pesona menggunakan ekspresi reguler mulai terwujud. Dengan cara yang cukup sederhana, kami telah mengidentifikasi urutan token yang kompleks: jumlah bukan-nol dari jumlah non-negatif yang kurang dari 1901 saling mengikuti, mungkin dipisahkan oleh penyatuan "dan", dan baik nama bulan atau kata "angka" berikut.
Selain itu, dalam grup pertandingan, kami segera menangkap elemen tertentu, jika ada, dan tidak menangkap jika tidak ada, dan dari set ini kami menyusun tanggal akhir. Dalam kode di bawah ini, akan ada bagian yang tidak dapat dipahami terkait dengan fungsi Fix
, FixPeriod
, kita akan beralih ke ini di akhir artikel.
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; |
| } |
| } |
Selang waktu
"(i)?((0?[Ymwdhe]N?)+)([bl])?";
Penting untuk dicatat bahwa kadang-kadang hanya fakta kebetulan dengan musim reguler tidak cukup, dan Anda masih perlu menambahkan beberapa logika ke pawang. Meskipun, dimungkinkan untuk membuat dua penangan terpisah untuk kasus-kasus seperti itu, tetapi bagi saya itu kelihatannya berlebihan. Sebagai hasilnya, saya mencocokkan awal "melalui" dan akhir "nanti / mundur", tetapi kode pawang dimulai dengan cek:
if (match.Groups[1].Success ^ match.Groups[4].Success)
Di mana ^
adalah OR eksklusif.
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; |
| } |
| } |
Omong-omong, agar fungsi-fungsi tersebut berfungsi, engine perlu mentransfer tanggal pengguna saat ini.
Tahun
"(1)Y?|(0)Y";
Saya tidak bisa sekali lagi mencatat kenyamanan pendekatan dengan pelanggan tetap. Sekali lagi, dengan ekspresi sederhana, kami segera menunjukkan opsi: pengguna menyebutkan nomor yang mirip dengan tahun, tetapi tanpa kata "tahun", atau pengguna menyebutkan nomor dua digit (yang bisa berupa tanggal), tetapi menambahkan kata "tahun", dan dukungan untuk ekspresi "di 18 tahun, "namun, kami tidak tahu apakah artinya 1918 atau 2018, jadi kami percaya bahwa 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; |
| } |
| } |
Waktu
"([rvgd])?([fot])?(Q|H)?(h|(0)(h)?)((0)e?)?([rvgd])?";
Ungkapan yang paling sulit dalam koleksi saya bertanggung jawab atas frasa untuk menunjukkan waktu hari, dari ketat pada 9 jam 30 menit hingga seperempat hingga 11 malam
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; |
| } |
| } |
Semua penangan dalam urutan aplikasi
Sistem ini modular, dapat dipahami bahwa Anda dapat menambah / menghapus penangan, mengubah urutannya, dan sebagainya. Saya membuat 11 ini (dari atas ke bawah dalam urutan aplikasi):
1 "" "" "" " ",
Jahit bersama
Oke, kami mendefinisikan beberapa token waktu-waktu dasar. Tetapi sekarang kita masih perlu memperhitungkan semua kombinasi dari semua jenis token: pengguna dapat memberi nama hari pertama, dan kemudian waktu. Atau sebutkan bulan dan hari. Jika dia mengatakan hanya waktu, maka mungkin ini tentang hari ini, dan seterusnya. Untuk melakukan ini, kami datang dengan konsep fiksasi .
Perbaikan dalam kasus kami adalah bitmask di dalam setiap token waktu-tanggal, yang menunjukkan elemen tanggal dan waktu yang ditetapkan di dalamnya.
public enum FixPeriod { None = 0, Time = 1, TimeUncertain = 2, Day = 4, Week = 8, Month = 16, Year = 32 }
TimeUncertain
adalah komitmen untuk menunjukkan waktu setelah mana penyempurnaan dapat mengikuti, misalnya, di malam hari / di pagi hari . Tidak dapat dikatakan pukul 18 di pagi hari , tetapi dapat dikatakan jam 6 di malam hari , oleh karena itu angka 6
tersebut, seolah-olah, kurang βpastiβ tentang apa artinya daripada angka 18
.
Selanjutnya, kita akan menggunakan dua aturan:
- Jika tanggalnya dekat, dan mereka tidak memiliki bit yang sama, maka kami menggabungkannya menjadi satu
- Jika tanggalnya dekat, dan kami tidak menggabungkannya, tetapi salah satu dari mereka memiliki bit yang tidak dimiliki oleh yang kedua, maka kami menempati bit (dan nilai yang sesuai dengan mereka) dari yang di mana mereka berada, ke yang di mana mereka tidak, tetapi hanya untuk yang lebih besar: yaitu, kita tidak dapat mengambil satu hari jika hanya satu bulan diberikan, tetapi kita dapat mengambil satu bulan jika hanya satu hari yang ditentukan.

Jadi, jika pengguna berbicara pada hari Senin jam 9 malam , maka token pada hari Senin , di mana hanya hari yang ditetapkan, dikombinasikan dengan token pada jam 9 malam , di mana waktu ditetapkan. Tetapi jika ia mengatakan pada 10 dan 15 Maret hari itu , maka token terakhir pada 15 hari itu hanya akan mengambil bulan Maret dari token sebelumnya.
Serikat pekerja itu sepele, tetapi dengan pinjaman kita akan melanjutkan saja. Kami akan memanggil tanggal dasar yang kami tempati, dan yang kedua, dari mana kami ingin meminjam sesuatu. Kemudian kami menyalin tanggal sekunder dan hanya menyisakan bit yang tidak dimiliki basis:
var baseDate = data.Dates[firstIndex]; var secondDate = data.Dates[secondIndex]; var secondCopy = secondDate.CopyOf(); secondCopy.Fixed &= (byte)~baseDate.Fixed;
Misalnya, jika pangkalan memiliki satu tahun, kami tidak akan meninggalkan fiksasi tahun untuk salinan anak di bawah umur (bahkan jika sudah). Jika pangkalan tidak memiliki satu bulan, tetapi sekunder memilikinya, ia akan tetap dengan salinan sebagai sekunder. Setelah itu, kami melakukan proses penggabungan tanggal dasar dan salinan sekunder.
Ketika digabungkan, kami juga memiliki tanggal dasar dan tanggal yang diserap . Kami bergerak dari atas ke bawah dari periode terbesar (tahun) ke terkecil (waktu). Jika tanggal dasar tidak memiliki beberapa parameter yang diserap, kami menambahkannya ke dasar yang diserap.
Ciutkan.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; |
| } |
Secara terpisah, Anda perlu mempertimbangkan beberapa nuansa:
- Tanggal dasar mungkin memiliki satu hari tetap, tetapi tidak seminggu. Dalam hal ini, Anda perlu mengambil satu minggu dari tanggal yang diserap, tetapi hari dalam seminggu dari pangkalan.
- Namun, jika tanggal yang diserap tidak memiliki hari yang tetap, tetapi satu minggu tetap, kita perlu mengambil hari darinya (yaitu, tahun + bulan + tanggal) dan mengatur minggu dengan cara ini, karena tidak ada entitas "Minggu" yang terpisah di objek
DateTime
. - Jika
TimeUncertain
ditetapkan pada tanggal dasar, dan Time
diserap pada yang diserap, dan jumlah jam pada yang diserap lebih dari 12, sedangkan yang dasar lebih sedikit, kemudian tambahkan 12 jam ke dasar. Karena Anda tidak bisa mengatakan dari 5 hingga 17 di malam hari . Waktu tanggal "tidak pasti" tidak boleh dari setengah hari, jika waktu tanggal "percaya diri" di sebelahnya adalah dari setengah hari lainnya. Orang tidak mengatakan itu. Jika kita mengucapkan frasa dari jam 5 pagi sampai jam 5 sore , maka kedua tanggal memiliki waktu "percaya diri", dan tidak ada masalah.
Setelah menggabungkan, jika beberapa tanggal memiliki bit kosong, kami menggantinya dengan nilai dari tanggal pengguna saat ini: misalnya, jika pengguna tidak menyebutkan tahun, kami berbicara tentang tahun saat ini, jika Anda tidak menyebutkan bulan, lalu bulan saat ini dan seterusnya. Tentu saja, objek DateTime
apa pun dapat diteruskan ke parameter "date date" untuk fleksibilitas.
Kruk
Tidak semua momen perpustakaan dipikirkan dengan anggun. Agar berfungsi dengan benar, kruk berikut ditambahkan:
- Tanggal digabungkan secara terpisah untuk token yang dimulai dengan "b \ s \ co" dan secara terpisah untuk token yang dimulai dengan "b \ to \ on". Karena, jika pengguna menyebutkan periode, maka tidak mungkin untuk menggabungkan token dari tanggal mulai dan tanggal akhir.
- Saya tidak mulai memperkenalkan tingkat fiksasi terpisah untuk hari dalam seminggu, karena itu diperlukan di satu tempat: di mana itu jelas diberikan oleh sebuah kata dengan nama hari dalam seminggu. Membuat bendera untuk ini.
- Lari terpisah menggabungkan tanggal yang berada pada jarak tertentu dari satu sama lain. Ini dikendalikan oleh parameter
collapseDistance
, yang secara default adalah 4 token. Misalnya, frasa tersebut akan berfungsi: Lusa, pertemuan dengan seorang teman di usia 12 . Tapi itu tidak akan berhasil: lusa saya akan bertemu teman saya yang tercinta dan berusia 12 tahun .
Ringkasan

- Anda dapat menggunakan perpustakaan di proyek Anda. Dia sudah mengatasi banyak pilihan, tetapi saya memperbaiki dan memperbaikinya. Permintaan tarik dengan tes dipersilakan. Secara umum, datang dengan tes yang terdengar realistis (seperti yang orang katakan dalam hidup), tetapi itu merusak perpustakaan.
- Demo langsung di .NET Fiddle juga berfungsi, meskipun kode tersebut diduga digarisbawahi dengan kesalahan, tetapi dimulai. Di bagian bawah konsol, Anda dapat memasukkan frasa dalam bahasa Rusia, tidak lupa bahwa angka harus berupa angka.
- Demo yang sama dengan aplikasi konsol
Live ternyata seperti ini: