diff --git a/rules/ru/date.go b/rules/ru/date.go new file mode 100644 index 0000000..0f141d7 --- /dev/null +++ b/rules/ru/date.go @@ -0,0 +1,66 @@ +package ru + +import ( + "regexp" + "strconv" + "strings" + "time" + + "github.com/olebedev/when/rules" + "github.com/pkg/errors" +) + +// https://go.dev/play/p/YsVdaraCwIP + +func Date(s rules.Strategy) rules.Rule { + return &rules.F{ + RegExp: regexp.MustCompile(`(?i)(?:\b|^)(\d{1,2})\s*(` + MONTHS_PATTERN + `)(?:\s*(\d{4}))?(?:\s*в\s*(\d{1,2}):(\d{2}))?(?:\b|$)`), + Applier: func(m *rules.Match, c *rules.Context, o *rules.Options, ref time.Time) (bool, error) { + if (c.Day != nil || c.Month != nil || c.Year != nil) || s != rules.Override { + return false, nil + } + + day, err := strconv.Atoi(m.Captures[0]) + if err != nil { + return false, errors.Wrap(err, "date rule: day") + } + + month, ok := MONTHS[strings.ToLower(m.Captures[1])] + if !ok { + return false, errors.New("date rule: invalid month") + } + + year := time.Now().Year() + if m.Captures[2] != "" { + year, err = strconv.Atoi(m.Captures[2]) + if err != nil { + return false, errors.Wrap(err, "date rule: year") + } + } + + hour, minute := 0, 0 + if m.Captures[3] != "" && m.Captures[4] != "" { + hour, err = strconv.Atoi(m.Captures[3]) + if err != nil { + return false, errors.Wrap(err, "date rule: hour") + } + minute, err = strconv.Atoi(m.Captures[4]) + if err != nil { + return false, errors.Wrap(err, "date rule: minute") + } + } + + c.Day = &day + c.Month = pointerToInt(int(month)) + c.Year = &year + c.Hour = &hour + c.Minute = &minute + + return true, nil + }, + } +} + +func pointerToInt(v int) *int { + return &v +} diff --git a/rules/ru/date_test.go b/rules/ru/date_test.go new file mode 100644 index 0000000..f6241df --- /dev/null +++ b/rules/ru/date_test.go @@ -0,0 +1,41 @@ +package ru_test + +import ( + "github.com/olebedev/when" + "github.com/olebedev/when/rules" + "github.com/olebedev/when/rules/ru" + "testing" + "time" +) + +func TestDate(t *testing.T) { + w := when.New(nil) + w.Add(ru.Date(rules.Override)) + + fixt := []Fixture{ + // Simple dates + {"встреча 15 января 2024", 15, "15 января 2024", time.Date(2024, 1, 15, 0, 0, 0, 0, time.UTC).Sub(null)}, + {"5 марта 2025 запланирована встреча", 0, "5 марта 2025", time.Date(2025, 3, 5, 0, 0, 0, 0, time.UTC).Sub(null)}, + {"31 декабря 2023", 0, "31 декабря 2023", time.Date(2023, 12, 31, 0, 0, 0, 0, time.UTC).Sub(null)}, + + // Dates with time + {"15 января 2024 в 9:30", 0, "15 января 2024 в 9:30", time.Date(2024, 1, 15, 9, 30, 0, 0, time.UTC).Sub(null)}, + {"5 марта 2025 в 15:00 запланирована встреча", 0, "5 марта 2025 в 15:00", time.Date(2025, 3, 5, 15, 0, 0, 0, time.UTC).Sub(null)}, + {"31 декабря 2023 в 23:59", 0, "31 декабря 2023 в 23:59", time.Date(2023, 12, 31, 23, 59, 0, 0, time.UTC).Sub(null)}, + } + + ApplyFixtures(t, "ru.Date", w, fixt) +} + +func TestDateNil(t *testing.T) { + w := when.New(nil) + w.Add(ru.Date(rules.Override)) + + fixt := []Fixture{ + {"это текст без даты", 0, "", 0}, + {"15", 0, "", 0}, + {"15 чего-то", 0, "", 0}, + } + + ApplyFixturesNil(t, "ru.Date nil", w, fixt) +} diff --git a/rules/ru/dot_date_time.go b/rules/ru/dot_date_time.go new file mode 100644 index 0000000..2bfde47 --- /dev/null +++ b/rules/ru/dot_date_time.go @@ -0,0 +1,61 @@ +package ru + +import ( + "regexp" + "strconv" + "time" + + "github.com/olebedev/when/rules" + "github.com/pkg/errors" +) + +// https://go.dev/play/p/vRzLhHHupUJ + +func DotDateTime(s rules.Strategy) rules.Rule { + return &rules.F{ + RegExp: regexp.MustCompile(`(?i)(?:^|\b)(\d{2})\.(\d{2})\.(\d{4})(?:\s+(\d{2}):(\d{2}))?(?:\b|$)`), + Applier: func(m *rules.Match, c *rules.Context, o *rules.Options, ref time.Time) (bool, error) { + if (c.Day != nil || c.Month != nil || c.Year != nil || c.Hour != nil || c.Minute != nil) && s != rules.Override { + return false, nil + } + + day, err := strconv.Atoi(m.Captures[0]) + if err != nil { + return false, errors.Wrap(err, "dot date time rule: day") + } + + month, err := strconv.Atoi(m.Captures[1]) + if err != nil { + return false, errors.Wrap(err, "dot date time rule: month") + } + + year, err := strconv.Atoi(m.Captures[2]) + if err != nil { + return false, errors.Wrap(err, "dot date time rule: year") + } + + hour, minute := 0, 0 + if m.Captures[3] != "" && m.Captures[4] != "" { + hour, err = strconv.Atoi(m.Captures[3]) + if err != nil { + return false, errors.Wrap(err, "dot date time rule: hour") + } + minute, err = strconv.Atoi(m.Captures[4]) + if err != nil { + return false, errors.Wrap(err, "dot date time rule: minute") + } + } + + if day > 0 && day <= 31 && month > 0 && month <= 12 { + c.Day = &day + c.Month = &month + c.Year = &year + c.Hour = &hour + c.Minute = &minute + return true, nil + } + + return false, nil + }, + } +} diff --git a/rules/ru/dot_date_time_test.go b/rules/ru/dot_date_time_test.go new file mode 100644 index 0000000..347b395 --- /dev/null +++ b/rules/ru/dot_date_time_test.go @@ -0,0 +1,37 @@ +package ru_test + +import ( + "github.com/olebedev/when" + "github.com/olebedev/when/rules" + "github.com/olebedev/when/rules/ru" + "testing" + "time" +) + +func TestDotDateTime(t *testing.T) { + w := when.New(nil) + w.Add(ru.DotDateTime(rules.Override)) + + fixt := []Fixture{ + // Basic date/time formats + {"встреча 15.01.2024 09:30", 15, "15.01.2024 09:30", time.Date(2024, 1, 15, 9, 30, 0, 0, time.UTC).Sub(null)}, + {"05.03.2025 15:00 запланирована встреча", 0, "05.03.2025 15:00", time.Date(2025, 3, 5, 15, 0, 0, 0, time.UTC).Sub(null)}, + {"31.12.2023 23:59", 0, "31.12.2023 23:59", time.Date(2023, 12, 31, 23, 59, 0, 0, time.UTC).Sub(null)}, + } + + ApplyFixtures(t, "ru.DateTime", w, fixt) +} + +func TestDotDateTimeNil(t *testing.T) { + w := when.New(nil) + w.Add(ru.DotDateTime(rules.Override)) + + fixt := []Fixture{ + {"это текст без даты и времени", 0, "", 0}, + {"15.01", 0, "", 0}, + {"32.01.2024 15:00", 0, "", 0}, // некорректный день + {"15.13.2024 15:00", 0, "", 0}, // некорректный месяц + } + + ApplyFixturesNil(t, "ru.DateTime nil", w, fixt) +} diff --git a/rules/ru/hour_minute.go b/rules/ru/hour_minute.go index fe4d51b..0703406 100644 --- a/rules/ru/hour_minute.go +++ b/rules/ru/hour_minute.go @@ -22,7 +22,7 @@ import ( {"11.1pm", 0, "11.1pm", 0}, {"11.10 pm", 0, "11.10 pm", 0}, - https://play.golang.org/p/PmPBjHK4PA + https://go.dev/play/p/QiSvUkrni6N */ // 1. - int @@ -31,12 +31,12 @@ import ( func HourMinute(s rules.Strategy) rules.Rule { return &rules.F{ - RegExp: regexp.MustCompile("(?i)(?:\\W|\\D|^)" + + RegExp: regexp.MustCompile("(?i)(?:\\A|\\s|\\D)" + "((?:[0-1]{0,1}[0-9])|(?:2[0-3]))" + "(?:\\:|:|\\-|\\.)" + "((?:[0-5][0-9]))" + "(?:\\s*(утра|вечера|дня))?" + - "(?:\\P{L}|$)"), + "(?:\\s|\\D|\\z)"), Applier: func(m *rules.Match, c *rules.Context, o *rules.Options, ref time.Time) (bool, error) { if (c.Hour != nil || c.Minute != nil) && s != rules.Override { return false, nil diff --git a/rules/ru/ru.go b/rules/ru/ru.go index b503d85..4e8afe2 100644 --- a/rules/ru/ru.go +++ b/rules/ru/ru.go @@ -1,6 +1,9 @@ package ru -import "github.com/olebedev/when/rules" +import ( + "github.com/olebedev/when/rules" + "time" +) var All = []rules.Rule{ Weekday(rules.Override), @@ -9,6 +12,8 @@ var All = []rules.Rule{ Hour(rules.Override), HourMinute(rules.Override), Deadline(rules.Override), + Date(rules.Override), + DotDateTime(rules.Override), } var WEEKDAY_OFFSET = map[string]int{ @@ -18,29 +23,29 @@ var WEEKDAY_OFFSET = map[string]int{ "понедельник": 1, "понедельнику": 1, "понедельника": 1, - "пн": 1, - "вторник": 2, - "вторника": 2, - "вторнику": 2, - "вт": 2, - "среда": 3, - "среду": 3, - "среде": 3, - "ср": 3, - "четверг": 4, - "четверга": 4, - "четвергу": 4, - "чт": 4, - "пятница": 5, - "пятнице": 5, - "пятницы": 5, - "пятницу": 5, - "пт": 5, - "суббота": 6, - "субботы": 6, - "субботе": 6, - "субботу": 6, - "сб": 6, + "пн": 1, + "вторник": 2, + "вторника": 2, + "вторнику": 2, + "вт": 2, + "среда": 3, + "среду": 3, + "среде": 3, + "ср": 3, + "четверг": 4, + "четверга": 4, + "четвергу": 4, + "чт": 4, + "пятница": 5, + "пятнице": 5, + "пятницы": 5, + "пятницу": 5, + "пт": 5, + "суббота": 6, + "субботы": 6, + "субботе": 6, + "субботу": 6, + "сб": 6, } var WEEKDAY_OFFSET_PATTERN = "(?:воскресенье|воскресенья|воск|понедельник|понедельнику|понедельника|пн|вторник|вторника|вторнику|вт|среда|среду|среде|ср|четверг|четверга|четвергу|чт|пятница|пятнице|пятницы|пятницу|пт|суббота|субботы|субботе|субботу|сб)" @@ -65,3 +70,20 @@ var INTEGER_WORDS = map[string]int{ } var INTEGER_WORDS_PATTERN = `(?:час|один|одну|одного|два|две|три|четыре|пять|шесть|семь|восемь|девять|десять|одиннадцать|двенадцать)` + +var MONTHS = map[string]time.Month{ + "января": time.January, + "февраля": time.February, + "марта": time.March, + "апреля": time.April, + "мая": time.May, + "июня": time.June, + "июля": time.July, + "августа": time.August, + "сентября": time.September, + "октября": time.October, + "ноября": time.November, + "декабря": time.December, +} + +var MONTHS_PATTERN = `(?:января|февраля|марта|апреля|мая|июня|июля|августа|сентября|октября|ноября|декабря)` diff --git a/rules/ru/ru_test.go b/rules/ru/ru_test.go index e4fe0dc..d41988e 100644 --- a/rules/ru/ru_test.go +++ b/rules/ru/ru_test.go @@ -66,6 +66,18 @@ func TestAll(t *testing.T) { {"написать письмо до утра субботы ", 30, "до утра субботы", ((3 * 24) + 8) * time.Hour}, {"написать письмо к субботе после обеда ", 30, "к субботе после обеда", ((3 * 24) + 15) * time.Hour}, {"В субботу вечером", 0, "В субботу вечером", ((3 * 24) + 18) * time.Hour}, + + {"встреча 15 января 2024", 15, "15 января 2024", time.Date(2024, 1, 15, 0, 0, 0, 0, time.UTC).Sub(null)}, + {"5 марта 2025 запланирована встреча", 0, "5 марта 2025", time.Date(2025, 3, 5, 0, 0, 0, 0, time.UTC).Sub(null)}, + {"31 декабря 2023", 0, "31 декабря 2023", time.Date(2023, 12, 31, 0, 0, 0, 0, time.UTC).Sub(null)}, + {"15 января 2024 в 9:30", 0, "15 января 2024 в 9:30", time.Date(2024, 1, 15, 9, 30, 0, 0, time.UTC).Sub(null)}, + {"5 марта 2025 в 15:00 запланирована встреча", 0, "5 марта 2025 в 15:00", time.Date(2025, 3, 5, 15, 0, 0, 0, time.UTC).Sub(null)}, + {"31 декабря 2023 в 23:59", 0, "31 декабря 2023 в 23:59", time.Date(2023, 12, 31, 23, 59, 0, 0, time.UTC).Sub(null)}, + {"31 декабря", 0, "31 декабря", time.Date(time.Now().Year(), 12, 31, 0, 0, 0, 0, time.UTC).Sub(null)}, + {"встреча 15.01.2024 09:30", 15, "15.01.2024 09:30", time.Date(2024, 1, 15, 9, 30, 0, 0, time.UTC).Sub(null)}, + {"05.03.2025 15:00 запланирована встреча", 0, "05.03.2025 15:00", time.Date(2025, 3, 5, 15, 0, 0, 0, time.UTC).Sub(null)}, + {"31.12.2023 23:59", 0, "31.12.2023 23:59", time.Date(2023, 12, 31, 23, 59, 0, 0, time.UTC).Sub(null)}, + {"31.12.2023", 0, "31.12.2023", time.Date(2023, 12, 31, 0, 0, 0, 0, time.UTC).Sub(null)}, } ApplyFixtures(t, "ru.All...", w, fixt) diff --git a/rules/zh/casual_date_test.go b/rules/zh/casual_date_test.go index 09bd0fc..d91fa98 100644 --- a/rules/zh/casual_date_test.go +++ b/rules/zh/casual_date_test.go @@ -10,8 +10,6 @@ import ( ) func TestCasualDate(t *testing.T) { - // current is Monday - now := time.Now() fixt := []Fixture{ {"后天中午", 0, "后天", (2 * 24) * time.Hour}, {"大后天中午", 0, "大后天", (3 * 24) * time.Hour},