diff --git a/rules/nl/casual_date.go b/rules/nl/casual_date.go new file mode 100644 index 0000000..18924e0 --- /dev/null +++ b/rules/nl/casual_date.go @@ -0,0 +1,69 @@ +package nl + +import ( + "regexp" + "strings" + "time" + + "github.com/AlekSi/pointer" + "github.com/olebedev/when/rules" +) + +func CasualDate(s rules.Strategy) rules.Rule { + overwrite := s == rules.Override + + return &rules.F{ + RegExp: regexp.MustCompile("(?i)(?:\\W|^)(nu|vandaag|vanavond|vannacht|afgelopen\\s*nacht|morgen|gister|gisteren)(ochtend|morgen|middag|avond)?(?:\\W|$)"), + Applier: func(m *rules.Match, c *rules.Context, o *rules.Options, ref time.Time) (bool, error) { + lower := strings.ToLower(strings.TrimSpace(m.String())) + + if regexp.MustCompile("ochtend|\\s*morgen|middag|avond").MatchString(lower) { + switch { + case strings.Contains(lower, "ochtend"), regexp.MustCompile("(?i)(?:\\W|^)(\\s*morgen)(?:\\W|$)").MatchString(lower): + if o.Morning != 0 { + c.Hour = &o.Morning + } else { + c.Hour = pointer.ToInt(8) + } + case strings.Contains(lower, "middag"): + if o.Afternoon != 0 { + c.Hour = &o.Afternoon + } else { + c.Hour = pointer.ToInt(15) + } + case strings.Contains(lower, "avond"): + if o.Evening != 0 { + c.Hour = &o.Evening + } else { + c.Hour = pointer.ToInt(18) + } + } + } + + switch { + case strings.Contains(lower, "vannacht"): + if c.Hour == nil && c.Minute == nil || overwrite { + c.Hour = pointer.ToInt(23) + c.Minute = pointer.ToInt(0) + } + case strings.Contains(lower, "vandaag"): + // c.Hour = pointer.ToInt(18) + case strings.Contains(lower, "morgen"): + if c.Duration == 0 || overwrite { + c.Duration += time.Hour * 24 + } + case strings.Contains(lower, "gister"): + if c.Duration == 0 || overwrite { + c.Duration -= time.Hour * 24 + } + case strings.Contains(lower, "afgelopen nacht"): + if (c.Hour == nil && c.Duration == 0) || overwrite { + c.Hour = pointer.ToInt(23) + c.Duration -= time.Hour * 24 + } + } + + return true, nil + }, + } +} diff --git a/rules/nl/casual_test.go b/rules/nl/casual_test.go new file mode 100644 index 0000000..b1856be --- /dev/null +++ b/rules/nl/casual_test.go @@ -0,0 +1,57 @@ +package nl_test + +import ( + "github.com/olebedev/when/rules/nl" + "testing" + "time" + + "github.com/olebedev/when" + "github.com/olebedev/when/rules" +) + +func TestCasualDate(t *testing.T) { + fixt := []Fixture{ + {"De deadline is nu, ok", 15, "nu", 0}, + {"De deadline is vandaag", 15, "vandaag", 0}, + {"De deadline is vannacht", 15, "vannacht", 23 * time.Hour}, + {"De deadline is morgenavond", 15, "morgenavond", (18 + 24) * time.Hour}, + {"De deadline is gisteravond", 15, "gisteravond", -((24 - 18) * time.Hour)}, + {"De deadline is gisteren", 15, "gisteren", -(time.Hour * 24)}, + } + + w := when.New(nil) + w.Add(nl.CasualDate(rules.Skip)) + + ApplyFixtures(t, "nl.CasualDate", w, fixt) +} + +func TestCasualTime(t *testing.T) { + fixt := []Fixture{ + {"De deadline was deze morgen", 16, "deze morgen", 8 * time.Hour}, + {"De deadline was tussen de middag", 16, "tussen de middag", 12 * time.Hour}, + {"De deadline was deze middag", 16, "deze middag", 15 * time.Hour}, + {"De deadline was deze avond", 16, "deze avond", 18 * time.Hour}, + {"De deadline is donderdagavond", 15, "donderdagavond", (18 + 24) * time.Hour}, + {"De deadline is vrijdagavond", 15, "vrijdagavond", (18 + 24*2) * time.Hour}, + } + + w := when.New(nil) + w.Add(nl.CasualTime(rules.Skip)) + + ApplyFixtures(t, "nl.CasualTime", w, fixt) +} + +func TestCasualDateCasualTime(t *testing.T) { + fixt := []Fixture{ + {"De deadline is morgenmiddag", 15, "morgenmiddag", (15 + 24) * time.Hour}, + {"De deadline is morgenavond", 15, "morgenavond", (18 + 24) * time.Hour}, + } + + w := when.New(nil) + w.Add( + nl.CasualDate(rules.Skip), + nl.CasualTime(rules.Override), + ) + + ApplyFixtures(t, "nl.CasualDate|nl.CasualTime", w, fixt) +} diff --git a/rules/nl/casual_time.go b/rules/nl/casual_time.go new file mode 100644 index 0000000..4ac1f65 --- /dev/null +++ b/rules/nl/casual_time.go @@ -0,0 +1,80 @@ +package nl + +import ( + "regexp" + "strings" + "time" + + "github.com/AlekSi/pointer" + "github.com/olebedev/when/rules" +) + +func CasualTime(s rules.Strategy) rules.Rule { + overwrite := s == rules.Override + + return &rules.F{ + RegExp: regexp.MustCompile(`(?i)(?:\W|^)((deze|tussen de |maandag|dinsdag|woensdag|donderdag|vrijdag|zaterdag|zondag| )\s*(ochtend|morgen|middag|avond))`), + Applier: func(m *rules.Match, c *rules.Context, o *rules.Options, ref time.Time) (bool, error) { + + lower := strings.ToLower(strings.TrimSpace(m.String())) + + if (c.Weekday != nil || c.Hour != nil || c.Minute != nil) && !overwrite { + return false, nil + } + + if regexp.MustCompile("(maandag|dinsdag|woensdag|donderdag|vrijdag|zaterdag|zondag)").MatchString(lower) { + weekday := -1 + + switch { + case strings.Contains(lower, "maandag"): + weekday = 1 + case strings.Contains(lower, "dinsdag"): + weekday = 2 + case strings.Contains(lower, "woensdag"): + weekday = 3 + case strings.Contains(lower, "donderdag"): + weekday = 4 + case strings.Contains(lower, "vrijdag"): + weekday = 5 + case strings.Contains(lower, "zaterdag"): + weekday = 6 + case strings.Contains(lower, "zondag"): + weekday = 7 + } + + if weekday != -1 { + c.Duration += time.Hour * 24 * time.Duration((weekday+7-(int(ref.Weekday())))%7) + } + } + + switch { + case strings.Contains(lower, "middag") && !strings.Contains(lower, "tussen de middag"): + if o.Afternoon != 0 { + c.Hour = &o.Afternoon + } else { + c.Hour = pointer.ToInt(15) + } + c.Minute = pointer.ToInt(0) + case strings.Contains(lower, "avond"): + if o.Evening != 0 { + c.Hour = &o.Evening + } else { + c.Hour = pointer.ToInt(18) + } + c.Minute = pointer.ToInt(0) + case strings.Contains(lower, "ochtend"), strings.Contains(lower, "morgen"): + if o.Morning != 0 { + c.Hour = &o.Morning + } else { + c.Hour = pointer.ToInt(8) + } + c.Minute = pointer.ToInt(0) + case strings.Contains(lower, "tussen de middag"): + c.Hour = pointer.ToInt(12) + c.Minute = pointer.ToInt(0) + } + + return true, nil + }, + } +} diff --git a/rules/nl/deadline.go b/rules/nl/deadline.go new file mode 100644 index 0000000..6f2212c --- /dev/null +++ b/rules/nl/deadline.go @@ -0,0 +1,106 @@ +package nl + +import ( + "regexp" + "strconv" + "strings" + "time" + + "github.com/AlekSi/pointer" + "github.com/olebedev/when/rules" + "github.com/pkg/errors" +) + +func Deadline(s rules.Strategy) rules.Rule { + overwrite := s == rules.Override + + return &rules.F{ + RegExp: regexp.MustCompile( + "(?i)(?:\\W|^)(binnen|in|over|na)\\s*" + + "(" + INTEGER_WORDS_PATTERN + "|[0-9]+|een(?:\\s*(paar|half|halve))?)\\s*" + + "(seconden?|minuut|minuten|uur|uren|dag|dagen|week|weken|maand|maanden|jaar|jaren)\\s*" + + "(?:\\W|$)"), + Applier: func(m *rules.Match, c *rules.Context, o *rules.Options, ref time.Time) (bool, error) { + numStr := strings.TrimSpace(m.Captures[1]) + + var num int + var err error + + if n, ok := INTEGER_WORDS[numStr]; ok { + num = n + } else if numStr == "een" { + num = 1 + } else if strings.Contains(numStr, "paar") { + num = 3 + } else if strings.Contains(numStr, "half") || strings.Contains(numStr, "halve") { + // pass + } else { + num, err = strconv.Atoi(numStr) + if err != nil { + return false, errors.Wrapf(err, "convert '%s' to int", numStr) + } + } + + exponent := strings.TrimSpace(m.Captures[3]) + + if !strings.Contains(numStr, "half") && !strings.Contains(numStr, "halve") { + switch { + case strings.Contains(exponent, "second"): + if c.Duration == 0 || overwrite { + c.Duration = time.Duration(num) * time.Second + } + case strings.Contains(exponent, "min"): + if c.Duration == 0 || overwrite { + c.Duration = time.Duration(num) * time.Minute + } + case strings.Contains(exponent, "uur"), strings.Contains(exponent, "uren"): + if c.Duration == 0 || overwrite { + c.Duration = time.Duration(num) * time.Hour + } + case strings.Contains(exponent, "dag"): + if c.Duration == 0 || overwrite { + c.Duration = time.Duration(num) * 24 * time.Hour + } + case strings.Contains(exponent, "week"), strings.Contains(exponent, "weken"): + if c.Duration == 0 || overwrite { + c.Duration = time.Duration(num) * 7 * 24 * time.Hour + } + case strings.Contains(exponent, "maand"): + if c.Month == nil || overwrite { + c.Month = pointer.ToInt((int(ref.Month()) + num) % 12) + } + case strings.Contains(exponent, "jaar"): + if c.Year == nil || overwrite { + c.Year = pointer.ToInt(ref.Year() + num) + } + } + } else { + switch { + case strings.Contains(exponent, "uur"): + if c.Duration == 0 || overwrite { + c.Duration = 30 * time.Minute + } + case strings.Contains(exponent, "dag"): + if c.Duration == 0 || overwrite { + c.Duration = 12 * time.Hour + } + case strings.Contains(exponent, "week"): + if c.Duration == 0 || overwrite { + c.Duration = 7 * 12 * time.Hour + } + case strings.Contains(exponent, "maand"): + if c.Duration == 0 || overwrite { + // 2 weeks + c.Duration = 14 * 24 * time.Hour + } + case strings.Contains(exponent, "jaar"): + if c.Month == nil || overwrite { + c.Month = pointer.ToInt((int(ref.Month()) + 6) % 12) + } + } + } + + return true, nil + }, + } +} diff --git a/rules/nl/deadline_test.go b/rules/nl/deadline_test.go new file mode 100644 index 0000000..0fc6f94 --- /dev/null +++ b/rules/nl/deadline_test.go @@ -0,0 +1,34 @@ +package nl_test + +import ( + "github.com/olebedev/when/rules/nl" + "testing" + "time" + + "github.com/olebedev/when" + "github.com/olebedev/when/rules" +) + +func TestDeadline(t *testing.T) { + fixt := []Fixture{ + {"binnen een half uur", 0, "binnen een half uur", time.Hour / 2}, + {"binnen 1 uur", 0, "binnen 1 uur", time.Hour}, + {"in 5 minuten", 0, "in 5 minuten", time.Minute * 5}, + {"Binnen 5 minuten ga ik naar huis", 0, "Binnen 5 minuten", time.Minute * 5}, + {"we moeten binnen 10 dagen iets doen", 10, "binnen 10 dagen", 10 * 24 * time.Hour}, + {"we moeten binnen vijf dagen iets doen", 10, "binnen vijf dagen", 5 * 24 * time.Hour}, + {"we moeten over 5 dagen iets doen", 10, "over 5 dagen", 5 * 24 * time.Hour}, + {"In 5 seconde moet een auto verplaatsen", 0, "In 5 seconde", 5 * time.Second}, + {"binnen twee weken", 0, "binnen twee weken", 14 * 24 * time.Hour}, + {"binnen een maand", 0, "binnen een maand", 31 * 24 * time.Hour}, + {"na een maand", 0, "na een maand", 31 * 24 * time.Hour}, + {"binnen een paar maanden", 0, "binnen een paar maanden", 91 * 24 * time.Hour}, + {"binnen een jaar", 0, "binnen een jaar", 366 * 24 * time.Hour}, + {"in een week", 0, "in een week", 7 * 24 * time.Hour}, + } + + w := when.New(nil) + w.Add(nl.Deadline(rules.Skip)) + + ApplyFixtures(t, "nl.Deadline", w, fixt) +} diff --git a/rules/nl/exact_month_date.go b/rules/nl/exact_month_date.go new file mode 100644 index 0000000..b61cc3b --- /dev/null +++ b/rules/nl/exact_month_date.go @@ -0,0 +1,101 @@ +package nl + +import ( + "regexp" + "strconv" + "strings" + "time" + + "github.com/olebedev/when/rules" +) + +// <[]string{"derde van maart", "derde", "", "maart", "", ""}> +// <[]string{"3e van march", "3e", "", "maart", "", ""}> +// <[]string{"1e van september", "1e", "", "september", "", ""}> +// <[]string{"1 sept.", "", "", "1", "sept", ""}> +// <[]string{"twintigste van december", "twintigste", "", "december", "", ""}> +// <[]string{"februari", "", "", "februari", "", ""}> +// <[]string{"oktober", "", "", "oktober", "", ""}> +// <[]string{"jul.", "", "", "jul.", "", ""}> +// <[]string{"juni", "", "", "juni", "", ""}> + +// https://play.golang.org/p/Zfjl6ERNkq + +// 1. - ordinal day? +// 2. - numeric day? +// 3. - month +// 4. - ordinal day? +// 5. - ordinal day? + +func ExactMonthDate(s rules.Strategy) rules.Rule { + overwrite := s == rules.Override + + return &rules.F{ + RegExp: regexp.MustCompile("(?i)" + + "(?:\\W|^)" + + "(?:(?:(" + ORDINAL_WORDS_PATTERN[3:] + "(?:\\s+van)?|([0-9]+))\\s*)?" + + "(" + MONTH_OFFSET_PATTERN[3:] + // skip '(?:' + "(?:\\s*(?:(" + ORDINAL_WORDS_PATTERN[3:] + "|([0-9]+)))?" + + "(?:\\W|$)", + ), + + Applier: func(m *rules.Match, c *rules.Context, o *rules.Options, ref time.Time) (bool, error) { + _ = overwrite + + ord1 := strings.ToLower(strings.TrimSpace(m.Captures[0])) + num1 := strings.ToLower(strings.TrimSpace(m.Captures[1])) + mon := strings.ToLower(strings.TrimSpace(m.Captures[2])) + ord2 := strings.ToLower(strings.TrimSpace(m.Captures[3])) + num2 := strings.ToLower(strings.TrimSpace(m.Captures[4])) + + monInt, ok := MONTH_OFFSET[mon] + if !ok { + return false, nil + } + + c.Month = &monInt + + if ord1 != "" { + ordInt, ok := ORDINAL_WORDS[ord1] + if !ok { + return false, nil + } + + c.Day = &ordInt + } + + if num1 != "" { + n, err := strconv.ParseInt(num1, 10, 8) + if err != nil { + return false, nil + } + + num := int(n) + + c.Day = &num + } + + if ord2 != "" { + ordInt, ok := ORDINAL_WORDS[ord2] + if !ok { + return false, nil + } + + c.Day = &ordInt + } + + if num2 != "" { + n, err := strconv.ParseInt(num2, 10, 8) + if err != nil { + return false, nil + } + + num := int(n) + + c.Day = &num + } + + return true, nil + }, + } +} diff --git a/rules/nl/exact_month_date_test.go b/rules/nl/exact_month_date_test.go new file mode 100644 index 0000000..c065b62 --- /dev/null +++ b/rules/nl/exact_month_date_test.go @@ -0,0 +1,31 @@ +package nl_test + +import ( + "github.com/olebedev/when/rules/nl" + "testing" + "time" + + "github.com/olebedev/when" + "github.com/olebedev/when/rules" +) + +func TestExactMonthDate(t *testing.T) { + w := when.New(nil) + w.Add(nl.ExactMonthDate(rules.Override)) + + fixtok := []Fixture{ + {"derde van maart", 0, "derde van maart", 1368 * time.Hour}, + {"3e van maart", 0, "3e van maart", 1368 * time.Hour}, + {"1 september", 0, "1 september", 5736 * time.Hour}, + {"1 sept", 0, "1 sept", 5736 * time.Hour}, + {"1 sept.", 0, "1 sept.", 5736 * time.Hour}, + {"1e van september", 0, "1e van september", 5736 * time.Hour}, + {"twintigste van december", 0, "twintigste van december", 8376 * time.Hour}, + {"februari", 0, "februari", 744 * time.Hour}, + {"oktober", 0, "oktober", 6576 * time.Hour}, + {"jul.", 0, "jul.", 4368 * time.Hour}, + {"juni", 0, "juni", 3648 * time.Hour}, + } + + ApplyFixtures(t, "nl.ExactMonthDate", w, fixtok) +} diff --git a/rules/nl/hour.go b/rules/nl/hour.go new file mode 100644 index 0000000..7f7b35f --- /dev/null +++ b/rules/nl/hour.go @@ -0,0 +1,72 @@ +package nl + +import ( + "regexp" + "strconv" + "strings" + "time" + + "github.com/pkg/errors" + + "github.com/olebedev/when/rules" +) + +/* + "5u" + "5 uur" + "5am" + "5pm" + "5A." + "5P." + "11 P.M." + https://play.golang.org/p/2Gh35Sl3KP +*/ + +func Hour(s rules.Strategy) rules.Rule { + return &rules.F{ + RegExp: regexp.MustCompile("(?i)(?:\\W|^)" + + "(?:\\s*((om)?))" + + "(\\d{1,2})" + + "(?:\\s*(U\\.?|UUR|A\\.|P\\.|A\\.M\\.|P\\.M\\.|AM?|PM?))" + + "(?:\\s*((in de|\\'s) (middags?|avonds?))?)" + + "(?:\\W|$)"), + Applier: func(m *rules.Match, c *rules.Context, o *rules.Options, ref time.Time) (bool, error) { + if c.Hour != nil && s != rules.Override { + return false, nil + } + + lower := strings.ToLower(strings.TrimSpace(m.String())) + hour, err := strconv.Atoi(m.Captures[2]) + + if err != nil { + return false, errors.Wrap(err, "hour rule") + } + + zero := 0 + + if hour > 23 { + return false, nil + } + c.Hour = &hour + + // pm + if regexp.MustCompile("p.?(m.?)?").MatchString(strings.ToLower(strings.TrimSpace(m.Captures[3]))) { + if hour < 12 { + hour += 12 + } + + c.Hour = &hour + } + + // afternoon or evening + if (strings.Contains(lower, "middag") || strings.Contains(lower, "avond")) && hour < 12 { + hour += 12 + c.Hour = &hour + } + + c.Minute = &zero + c.Second = &zero + return true, nil + }, + } +} diff --git a/rules/nl/hour_minute.go b/rules/nl/hour_minute.go new file mode 100644 index 0000000..3d781dd --- /dev/null +++ b/rules/nl/hour_minute.go @@ -0,0 +1,85 @@ +package nl + +import ( + "regexp" + "strconv" + "strings" + "time" + + "github.com/olebedev/when/rules" + "github.com/pkg/errors" +) + +/* + {"17:30", 0, "17:30", 0}, + {"17:30u", 0, "17:30u", 0}, + {"om 17:30 uur", 3, "17:30 uur", 0}, + {"om 5:59 pm", 3, "5:59 pm", 0}, + + https://play.golang.org/p/hXl7C8MWNr +*/ + +// 1. - at? +// 2. - int +// 3. - int +// 4. - ext? +// 5. - day part? + +func HourMinute(s rules.Strategy) rules.Rule { + return &rules.F{ + RegExp: regexp.MustCompile("(?i)(?:\\W|^)" + + "(?:\\s*((om)?))" + + "((?:[0-1]{0,1}[0-9])|(?:2[0-3]))" + + "(?:\\:|:)" + + "((?:[0-5][0-9]))" + + "(?:\\s*(U\\.?|UUR|A\\.|P\\.|A\\.M\\.|P\\.M\\.|AM?|PM?))?" + + "(?:\\s*((in de|\\'s) (middags?|avonds?))?)" + + "(?:\\W|$)"), + 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 + } + + lower := strings.ToLower(strings.TrimSpace(m.String())) + hour, err := strconv.Atoi(m.Captures[2]) + if err != nil { + return false, errors.Wrap(err, "hour minute rule") + } + + minutes, err := strconv.Atoi(m.Captures[3]) + if err != nil { + return false, errors.Wrap(err, "hour minute rule") + } + + if minutes > 59 { + return false, nil + } + c.Minute = &minutes + + if hour > 23 { + return false, nil + } + c.Hour = &hour + + // pm + if regexp.MustCompile("p.?(m.?)?").MatchString(strings.ToLower(strings.TrimSpace(m.Captures[4]))) { + if hour < 12 { + hour += 12 + } + + c.Hour = &hour + } + + // afternoon or evening + if (strings.Contains(lower, "middag") || strings.Contains(lower, "avond")) && hour < 12 { + hour += 12 + c.Hour = &hour + } + + seconds := 0 // Truncate seconds + c.Second = &seconds + + return true, nil + }, + } +} diff --git a/rules/nl/hour_minute_test.go b/rules/nl/hour_minute_test.go new file mode 100644 index 0000000..4909d39 --- /dev/null +++ b/rules/nl/hour_minute_test.go @@ -0,0 +1,44 @@ +package nl_test + +import ( + "github.com/olebedev/when/rules/nl" + "testing" + "time" + + "github.com/olebedev/when" + "github.com/olebedev/when/rules" +) + +func TestHourMinute(t *testing.T) { + w := when.New(nil) + w.Add(nl.HourMinute(rules.Override)) + + fixtok := []Fixture{ + {"17:30u", 0, "17:30u", (17 * time.Hour) + (30 * time.Minute)}, + {"om 17:30 uur", 3, "17:30 uur", (17 * time.Hour) + (30 * time.Minute)}, + {"om 5:59 pm", 3, "5:59 pm", (17 * time.Hour) + (59 * time.Minute)}, + {"om 5:59 am", 3, "5:59 am", (5 * time.Hour) + (59 * time.Minute)}, + } + + fixtnil := []Fixture{ + {"28:30pm", 0, "", 0}, + {"12:61u", 0, "", 0}, + {"24:10", 0, "", 0}, + } + + ApplyFixtures(t, "nl.HourMinute", w, fixtok) + ApplyFixturesNil(t, "on.HourMinute nil", w, fixtnil) + + w.Add(nl.Hour(rules.Skip)) + ApplyFixtures(t, "nl.HourMinute|nl.Hour", w, fixtok) + ApplyFixturesNil(t, "on.HourMinute|nl.Hour nil", w, fixtnil) + + w = when.New(nil) + w.Add( + nl.Hour(rules.Override), + nl.HourMinute(rules.Override), + ) + + ApplyFixtures(t, "nl.Hour|nl.HourMinute", w, fixtok) + ApplyFixturesNil(t, "on.Hour|nl.HourMinute nil", w, fixtnil) +} diff --git a/rules/nl/hour_test.go b/rules/nl/hour_test.go new file mode 100644 index 0000000..020d487 --- /dev/null +++ b/rules/nl/hour_test.go @@ -0,0 +1,31 @@ +package nl_test + +import ( + "github.com/olebedev/when" + "github.com/olebedev/when/rules" + "github.com/olebedev/when/rules/nl" + "testing" + "time" +) + +func TestHour(t *testing.T) { + fixt := []Fixture{ + {"5pm", 0, "5pm", 17 * time.Hour}, + {"5 uur in de avond", 0, "5 uur in de avond", 17 * time.Hour}, + {"5 uur 's avonds", 0, "5 uur 's avonds", 17 * time.Hour}, + {"om 17 uur", 3, "17 uur", 17 * time.Hour}, + {"om 5 P.", 3, "5 P.", 17 * time.Hour}, + {"om 12 P.", 3, "12 P.", 12 * time.Hour}, + {"om 1 P.", 3, "1 P.", 13 * time.Hour}, + {"om 5 am", 3, "5 am", 5 * time.Hour}, + {"om 5A", 3, "5A", 5 * time.Hour}, + {"om 5A.", 3, "5A.", 5 * time.Hour}, + {"5A.", 0, "5A.", 5 * time.Hour}, + {"11 P.M.", 0, "11 P.M.", 23 * time.Hour}, + } + + w := when.New(nil) + w.Add(nl.Hour(rules.Override)) + + ApplyFixtures(t, "nl.Hour", w, fixt) +} diff --git a/rules/nl/nl.go b/rules/nl/nl.go new file mode 100644 index 0000000..979ae0d --- /dev/null +++ b/rules/nl/nl.go @@ -0,0 +1,166 @@ +package nl + +import "github.com/olebedev/when/rules" + +var All = []rules.Rule{ + Weekday(rules.Override), + CasualDate(rules.Override), + CasualTime(rules.Override), + Hour(rules.Override), + HourMinute(rules.Override), + Deadline(rules.Override), + PastTime(rules.Override), + ExactMonthDate(rules.Override), +} + +var WEEKDAY_OFFSET = map[string]int{ + "zondag": 0, + "zon": 0, + "zo": 0, + "maandag": 1, + "maa": 1, + "ma": 1, + "dinsdag": 2, + "din": 2, + "di": 2, + "woensdag": 3, + "woe": 3, + "wo": 3, + "donderdag": 4, + "don": 4, + "do": 4, + "vrijdag": 5, + "vrij": 5, + "vr": 5, + "zaterdag": 6, + "zat": 6, + "za": 6, +} + +var WEEKDAY_OFFSET_PATTERN = "(?:zondag|zon|zo|maandag|maa|ma|dinsdag|din|di|woensdag|woe|wo|donderdag|don|do|vrijdag|vrij|vr|zaterdag|zat|za)" + +var MONTH_OFFSET = map[string]int{ + "january": 1, + "jan": 1, + "jan.": 1, + "februari": 2, + "feb": 2, + "feb.": 2, + "maart": 3, + "mrt": 3, + "mrt.": 3, + "april": 4, + "apr": 4, + "apr.": 4, + "mei": 5, + "juni": 6, + "jun": 6, + "jun.": 6, + "juli": 7, + "jul": 7, + "jul.": 7, + "augustus": 8, + "aug": 8, + "aug.": 8, + "september": 9, + "sep": 9, + "sep.": 9, + "sept": 9, + "sept.": 9, + "oktober": 10, + "okt": 10, + "okt.": 10, + "november": 11, + "nov": 11, + "nov.": 11, + "december": 12, + "dec": 12, + "dec.": 12, +} + +var MONTH_OFFSET_PATTERN = `(?:january|jan\.?|februari|feb\.?|maart|mrt\.?|april|apr\.?|mei|juni|jun\.?|juli|jul\.?|augustus|aug\.?|september|sept?\.?|oktober|okt\.?|november|nov\.?|december|dec\.?)` + +var INTEGER_WORDS = map[string]int{ + "een": 1, + "één": 1, + "twee": 2, + "drie": 3, + "vier": 4, + "vijf": 5, + "zes": 6, + "zeven": 7, + "acht": 8, + "negen": 9, + "tien": 10, + "elf": 11, + "twaalf": 12, +} + +var INTEGER_WORDS_PATTERN = `(?:een|één|twee|drie|vier|vijf|zes|zeven|acht|negen|tien|elf|twaalf)` + +var ORDINAL_WORDS = map[string]int{ + "eerste": 1, + "1e": 1, + "tweede": 2, + "2e": 2, + "derde": 3, + "3e": 3, + "vierde": 4, + "4e": 4, + "vijfde": 5, + "5e": 5, + "zesde": 6, + "6e": 6, + "zevende": 7, + "7e": 7, + "achtste": 8, + "8e": 8, + "negende": 9, + "9e": 9, + "tiende": 10, + "10e": 10, + "elfde": 11, + "11e": 11, + "twaalfde": 12, + "12e": 12, + "derdiende": 13, + "13e": 13, + "veertiende": 14, + "14e": 14, + "vijftiende": 15, + "15e": 15, + "zestiende": 16, + "16e": 16, + "zeventiende": 17, + "17e": 17, + "achttiende": 18, + "18e": 18, + "negentiende": 19, + "19e": 19, + "twintigste": 20, + "20e": 20, + "eenentwintigste": 21, + "21e": 21, + "tweeentwintigste": 22, + "22e": 22, + "drieentwintigste": 23, + "23e": 23, + "vierentwintigste": 24, + "24e": 24, + "vijfentwintigste": 25, + "25e": 25, + "zesentwintigste": 26, + "26e": 26, + "zevenentwintigste": 27, + "27e": 27, + "achtentwintigste": 28, + "28e": 28, + "negenentwintigste": 29, + "29e": 29, + "dertigste": 30, + "30e": 30, + "eenendertigste": 31, + "31e": 31, +} + +var ORDINAL_WORDS_PATTERN = `(?:1e|eerste|2e|tweede|3e|derde|4e|vierde|5e|vijfde|6e|zesde|7e|zevende|8e|achtste|9e|negende|10e|tiende|11e|elfde|12e|twaalfde|13e|derdiende|14e|veertiende|15e|vijftiende|16e|zestiende|17e|zeventiende|18e|achttiende|19e|negentiende|20e|twintigste|21e|eenentwintigste|22e|tweeentwintigste|23e|drieentwintigste|24e|vierentwintigste|25e|vijfentwintigste|26e|zesentwintigste|27e|zevenentwintigste|28e|achtentwintigste|29e|negenentwintigste|30e|dertigste|31e|eenendertigste)` diff --git a/rules/nl/nl_test.go b/rules/nl/nl_test.go new file mode 100644 index 0000000..12ef262 --- /dev/null +++ b/rules/nl/nl_test.go @@ -0,0 +1,63 @@ +package nl_test + +import ( + "github.com/olebedev/when/rules/nl" + "testing" + "time" + + "github.com/olebedev/when" + "github.com/stretchr/testify/require" +) + +var null = time.Date(2016, time.January, 6, 0, 0, 0, 0, time.UTC) + +type Fixture struct { + Text string + Index int + Phrase string + Diff time.Duration +} + +func ApplyFixtures(t *testing.T, name string, w *when.Parser, fixt []Fixture) { + for i, f := range fixt { + res, err := w.Parse(f.Text, null) + require.Nil(t, err, "[%s] err #%d", name, i) + require.NotNil(t, res, "[%s] res #%d", name, i) + require.Equal(t, f.Index, res.Index, "[%s] index #%d", name, i) + require.Equal(t, f.Phrase, res.Text, "[%s] text #%d", name, i) + require.Equal(t, f.Diff, res.Time.Sub(null), "[%s] diff #%d", name, i) + } +} + +func ApplyFixturesNil(t *testing.T, name string, w *when.Parser, fixt []Fixture) { + for i, f := range fixt { + res, err := w.Parse(f.Text, null) + require.Nil(t, err, "[%s] err #%d", name, i) + require.Nil(t, res, "[%s] res #%d", name, i) + } +} + +func ApplyFixturesErr(t *testing.T, name string, w *when.Parser, fixt []Fixture) { + for i, f := range fixt { + _, err := w.Parse(f.Text, null) + require.NotNil(t, err, "[%s] err #%d", name, i) + require.Equal(t, f.Phrase, err.Error(), "[%s] err text #%d", name, i) + } +} + +func TestAll(t *testing.T) { + w := when.New(nil) + w.Add(nl.All...) + + // complex cases + fixt := []Fixture{ + {"vanavond om 23:10", 0, "vanavond om 23:10", (23 * time.Hour) + (10 * time.Minute)}, + {"op vrijdagmiddag", 3, "vrijdagmiddag", ((2 * 24) + 15) * time.Hour}, + {"komende dinsdag om 14:00", 0, "komende dinsdag om 14:00", ((6 * 24) + 14) * time.Hour}, + {"komende dinsdag 2 uur 's middags", 0, "komende dinsdag 2 uur 's middags", ((6 * 24) + 14) * time.Hour}, + {"komende woensdag om 14:25", 0, "komende woensdag om 14:25", (((7 * 24) + 14) * time.Hour) + (25 * time.Minute)}, + {"om 11 uur afgelopen dinsdag", 3, "11 uur afgelopen dinsdag", -13 * time.Hour}, + } + + ApplyFixtures(t, "nl.All...", w, fixt) +} diff --git a/rules/nl/past_time.go b/rules/nl/past_time.go new file mode 100644 index 0000000..055d9c4 --- /dev/null +++ b/rules/nl/past_time.go @@ -0,0 +1,107 @@ +package nl + +import ( + "regexp" + "strconv" + "strings" + "time" + + "github.com/AlekSi/pointer" + "github.com/olebedev/when/rules" + "github.com/pkg/errors" +) + +func PastTime(s rules.Strategy) rules.Rule { + overwrite := s == rules.Override + + return &rules.F{ + RegExp: regexp.MustCompile( + "(?i)(?:\\W|^)\\s*" + + "(" + INTEGER_WORDS_PATTERN + "|[0-9]+|een(?:\\s*(paar|half|halve))?)\\s*" + + "(seconden?|minuut|minuten|uur|uren|dag|dagen|week|weken|maand|maanden|jaar|jaren) (geleden)\\s*" + + "(?:\\W|$)"), + Applier: func(m *rules.Match, c *rules.Context, o *rules.Options, ref time.Time) (bool, error) { + + numStr := strings.TrimSpace(m.Captures[0]) + + var num int + var err error + + if n, ok := INTEGER_WORDS[numStr]; ok { + num = n + } else if numStr == "een" { + num = 1 + } else if strings.Contains(numStr, "paar") { + num = 3 + } else if strings.Contains(numStr, "half") || strings.Contains(numStr, "halve") { + // pass + } else { + num, err = strconv.Atoi(numStr) + if err != nil { + return false, errors.Wrapf(err, "convert '%s' to int", numStr) + } + } + + exponent := strings.TrimSpace(m.Captures[2]) + + if !strings.Contains(numStr, "half") && !strings.Contains(numStr, "halve") { + switch { + case strings.Contains(exponent, "seconde"): + if c.Duration == 0 || overwrite { + c.Duration = -(time.Duration(num) * time.Second) + } + case strings.Contains(exponent, "min"): + if c.Duration == 0 || overwrite { + c.Duration = -(time.Duration(num) * time.Minute) + } + case strings.Contains(exponent, "uur"), strings.Contains(exponent, "uren"): + if c.Duration == 0 || overwrite { + c.Duration = -(time.Duration(num) * time.Hour) + } + case strings.Contains(exponent, "dag"): + if c.Duration == 0 || overwrite { + c.Duration = -(time.Duration(num) * 24 * time.Hour) + } + case strings.Contains(exponent, "week"), strings.Contains(exponent, "weken"): + if c.Duration == 0 || overwrite { + c.Duration = -(time.Duration(num) * 7 * 24 * time.Hour) + } + case strings.Contains(exponent, "maand"): + if c.Month == nil || overwrite { + c.Month = pointer.ToInt((int(ref.Month()) - num) % 12) + } + case strings.Contains(exponent, "jaar"): + if c.Year == nil || overwrite { + c.Year = pointer.ToInt(ref.Year() - num) + } + } + } else { + switch { + case strings.Contains(exponent, "uur"), strings.Contains(exponent, "uren"): + if c.Duration == 0 || overwrite { + c.Duration = -(30 * time.Minute) + } + case strings.Contains(exponent, "dag"): + if c.Duration == 0 || overwrite { + c.Duration = -(12 * time.Hour) + } + case strings.Contains(exponent, "week"): + if c.Duration == 0 || overwrite { + c.Duration = -(7 * 12 * time.Hour) + } + case strings.Contains(exponent, "maand"): + if c.Duration == 0 || overwrite { + // 2 weeks + c.Duration = -(14 * 24 * time.Hour) + } + case strings.Contains(exponent, "jaar"): + if c.Month == nil || overwrite { + c.Month = pointer.ToInt((int(ref.Month()) - 6) % 12) + } + } + } + + return true, nil + }, + } +} diff --git a/rules/nl/past_time_test.go b/rules/nl/past_time_test.go new file mode 100644 index 0000000..fd6d558 --- /dev/null +++ b/rules/nl/past_time_test.go @@ -0,0 +1,33 @@ +package nl_test + +import ( + "github.com/olebedev/when/rules/nl" + "testing" + "time" + + "github.com/olebedev/when" + "github.com/olebedev/when/rules" +) + +func TestPastTime(t *testing.T) { + fixt := []Fixture{ + {"een half uur geleden", 0, "een half uur geleden", -(time.Hour / 2)}, + {"1 uur geleden", 0, "1 uur geleden", -(time.Hour)}, + {"5 minuten geleden", 0, "5 minuten geleden", -(time.Minute * 5)}, + {"5 minuten geleden ging ik naar de dierentuin", 0, "5 minuten geleden", -(time.Minute * 5)}, + {"we deden iets 10 dagen geleden", 14, "10 dagen geleden", -(10 * 24 * time.Hour)}, + {"we deden iets vijf dagen geleden", 14, "vijf dagen geleden", -(5 * 24 * time.Hour)}, + {"we deden iets 5 dagen geleden", 14, "5 dagen geleden", -(5 * 24 * time.Hour)}, + {"5 seconden geleden werd een auto weggesleept", 0, "5 seconden geleden", -(5 * time.Second)}, + {"twee weken geleden", 0, "twee weken geleden", -(14 * 24 * time.Hour)}, + {"een maand geleden", 0, "een maand geleden", -(31 * 24 * time.Hour)}, + {"een paar maanden geleden", 0, "een paar maanden geleden", -(92 * 24 * time.Hour)}, + {"een jaar geleden", 0, "een jaar geleden", -(365 * 24 * time.Hour)}, + {"een week geleden", 0, "een week geleden", -(7 * 24 * time.Hour)}, + } + + w := when.New(nil) + w.Add(nl.PastTime(rules.Skip)) + + ApplyFixtures(t, "nl.PastTime", w, fixt) +} diff --git a/rules/nl/weekday.go b/rules/nl/weekday.go new file mode 100644 index 0000000..cc7592b --- /dev/null +++ b/rules/nl/weekday.go @@ -0,0 +1,86 @@ +package nl + +import ( + "regexp" + "strings" + "time" + + "github.com/olebedev/when/rules" +) + +func Weekday(s rules.Strategy) rules.Rule { + overwrite := s == rules.Override + + return &rules.F{ + RegExp: regexp.MustCompile("(?i)" + + "(?:\\W|^)" + + "(?:op\\s*?)?" + + "(?:(deze|vorige|afgelopen|volgende|komende)\\s*)?" + + "(" + WEEKDAY_OFFSET_PATTERN[3:] + // skip '(?:' + "(?:\\s*(deze|vorige|afgelopen|volgende|komende)\\s*week)?" + + "(?:\\W|$)", + ), + + Applier: func(m *rules.Match, c *rules.Context, o *rules.Options, ref time.Time) (bool, error) { + _ = overwrite + + day := strings.ToLower(strings.TrimSpace(m.Captures[1])) + norm := strings.ToLower(strings.TrimSpace(m.Captures[0] + m.Captures[2])) + if norm == "" { + norm = "volgende" + } + dayInt, ok := WEEKDAY_OFFSET[day] + if !ok { + return false, nil + } + + if c.Duration != 0 && !overwrite { + return false, nil + } + + // Switch: + switch { + case strings.Contains(norm, "afgelopen") || strings.Contains(norm, "vorige"): + diff := int(ref.Weekday()) - dayInt + if diff > 0 { + c.Duration = -time.Duration(diff*24) * time.Hour + } else if diff < 0 { + c.Duration = -time.Duration(7+diff) * 24 * time.Hour + } else { + c.Duration = -(7 * 24 * time.Hour) + } + case strings.Contains(norm, "volgende"), strings.Contains(norm, "komende"): + diff := dayInt - int(ref.Weekday()) + if diff > 0 { + c.Duration = time.Duration(diff*24) * time.Hour + } else if diff < 0 { + c.Duration = time.Duration(7+diff) * 24 * time.Hour + } else { + c.Duration = 7 * 24 * time.Hour + } + case strings.Contains(norm, "deze"): + if int(ref.Weekday()) < dayInt { + diff := dayInt - int(ref.Weekday()) + if diff > 0 { + c.Duration = time.Duration(diff*24) * time.Hour + } else if diff < 0 { + c.Duration = time.Duration(7+diff) * 24 * time.Hour + } else { + c.Duration = 7 * 24 * time.Hour + } + } else if int(ref.Weekday()) > dayInt { + diff := int(ref.Weekday()) - dayInt + if diff > 0 { + c.Duration = -time.Duration(diff*24) * time.Hour + } else if diff < 0 { + c.Duration = -time.Duration(7+diff) * 24 * time.Hour + } else { + c.Duration = -(7 * 24 * time.Hour) + } + } + } + + return true, nil + }, + } +} diff --git a/rules/nl/weekday_test.go b/rules/nl/weekday_test.go new file mode 100644 index 0000000..5d85cdf --- /dev/null +++ b/rules/nl/weekday_test.go @@ -0,0 +1,39 @@ +package nl_test + +import ( + "github.com/olebedev/when/rules/nl" + "testing" + "time" + + "github.com/olebedev/when" + "github.com/olebedev/when/rules" +) + +func TestWeekday(t *testing.T) { + // current is Friday + fixt := []Fixture{ + // past/last + {"doe het voor afgelopen maandag", 13, "afgelopen maandag", -(2 * 24 * time.Hour)}, + {"afgelopen zaterdag", 0, "afgelopen zaterdag", -(4 * 24 * time.Hour)}, + {"afgelopen vrijdag", 0, "afgelopen vrijdag", -(5 * 24 * time.Hour)}, + {"afgelopen woensdag", 0, "afgelopen woensdag", -(7 * 24 * time.Hour)}, + {"afgelopen dinsdag", 0, "afgelopen dinsdag", -(24 * time.Hour)}, + // next + {"komende dinsdag", 0, "komende dinsdag", 6 * 24 * time.Hour}, + {"stuur me een bericht komende woensdag", 21, "komende woensdag", 7 * 24 * time.Hour}, + {"komende zaterdag", 0, "komende zaterdag", 3 * 24 * time.Hour}, + {"volgende dinsdag", 0, "volgende dinsdag", 6 * 24 * time.Hour}, + {"stuur me een bericht volgende woensdag", 21, "volgende woensdag", 7 * 24 * time.Hour}, + {"volgende zaterdag", 0, "volgende zaterdag", 3 * 24 * time.Hour}, + // this + {"deze dinsdag", 0, "deze dinsdag", -(24 * time.Hour)}, + {"stuur me een bericht deze woensdag", 21, "deze woensdag", 0}, + {"deze zaterdag", 0, "deze zaterdag", 3 * 24 * time.Hour}, + } + + w := when.New(nil) + + w.Add(nl.Weekday(rules.Override)) + + ApplyFixtures(t, "nl.Weekday", w, fixt) +} diff --git a/when.go b/when.go index beb6626..ba537b4 100644 --- a/when.go +++ b/when.go @@ -5,10 +5,11 @@ import ( "time" "github.com/olebedev/when/rules" + "github.com/olebedev/when/rules/br" "github.com/olebedev/when/rules/common" "github.com/olebedev/when/rules/en" + "github.com/olebedev/when/rules/nl" "github.com/olebedev/when/rules/ru" - "github.com/olebedev/when/rules/br" "github.com/pkg/errors" ) @@ -153,6 +154,9 @@ var RU *Parser // BR is a parser for Brazilian Portuguese language var BR *Parser +// NL is a parser for Dutch language +var NL *Parser + func init() { EN = New(nil) EN.Add(en.All...) @@ -165,4 +169,8 @@ func init() { BR = New(nil) BR.Add(br.All...) BR.Add(common.All...) + + NL = New(nil) + NL.Add(nl.All...) + NL.Add(common.All...) }