Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions rules/en/en.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ var All = []rules.Rule{
Deadline(rules.Override),
PastTime(rules.Override),
ExactMonthDate(rules.Override),
Timezone(rules.Override),
}

var WEEKDAY_OFFSET = map[string]int{
Expand Down Expand Up @@ -167,3 +168,7 @@ var ORDINAL_WORDS = map[string]int{
}

var ORDINAL_WORDS_PATTERN = `(?:1st|first|2nd|second|3rd|third|4th|fourth|5th|fifth|6th|sixth|7th|seventh|8th|eighth|9th|ninth|10th|tenth|11th|eleventh|12th|twelfth|13th|thirteenth|14th|fourteenth|15th|fifteenth|16th|sixteenth|17th|seventeenth|18th|eighteenth|19th|nineteenth|20th|twentieth|21st|twenty[ -]first|22nd|twenty[ -]second|23rd|twenty[ -]third|24th|twenty[ -]fourth|25th|twenty[ -]fifth|26th|twenty[ -]sixth|27th|twenty[ -]seventh|28th|twenty[ -]eighth|29th|twenty[ -]ninth|30th|thirtieth|31st|thirty[ -]first)`

func init() {
// append Timezone rule if needed or include in All
}
84 changes: 84 additions & 0 deletions rules/en/timezone.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package en

import (
"errors"
"regexp"
"strings"
"time"

"github.com/olebedev/when/rules"
)

func Timezone(s rules.Strategy) rules.Rule {
return &rules.F{
RegExp: regexp.MustCompile("(?i)(?:\\W|^)" +
"((?:UTC|GMT|EST|EDT|CST|CDT|MST|MDT|PST|PDT|[+-]\\d{2}:?\\d{2}))" +
"(?:\\W|$)"),
Applier: func(m *rules.Match, c *rules.Context, o *rules.Options, ref time.Time) (bool, error) {
if c.Location != nil && s != rules.Override {
return false, nil
}

tzStr := m.Captures[0]
loc, err := parseTimezone(tzStr)
if err != nil {
return false, nil
}

c.Location = loc
return true, nil
},
}
}

func parseTimezone(tz string) (*time.Location, error) {
upper := strings.ToUpper(tz)
switch upper {
case "UTC", "GMT":
return time.UTC, nil
case "EST":
return time.FixedZone("EST", -5*3600), nil
case "EDT":
return time.FixedZone("EDT", -4*3600), nil
case "CST":
return time.FixedZone("CST", -6*3600), nil
case "CDT":
return time.FixedZone("CDT", -5*3600), nil
case "MST":
return time.FixedZone("MST", -7*3600), nil
case "MDT":
return time.FixedZone("MDT", -6*3600), nil
case "PST":
return time.FixedZone("PST", -8*3600), nil
case "PDT":
return time.FixedZone("PDT", -7*3600), nil
}

loc, err := time.LoadLocation(tz)
if err == nil {
return loc, nil
}

return parseNumericOffset(tz)
}

func parseNumericOffset(tz string) (*time.Location, error) {
re := regexp.MustCompile(`^([+-])(\d{2}):?(\d{2})?$`)
matches := re.FindStringSubmatch(tz)
if matches == nil {
return nil, errors.New("invalid timezone offset")
}

sign := matches[1]
hours, _ := time.ParseDuration(matches[2] + "h")
var minutes time.Duration
if matches[3] != "" {
minutes, _ = time.ParseDuration(matches[3] + "m")
}
offset := int(hours + minutes)
if sign == "-" {
offset = -offset
}

return time.FixedZone(tz, offset), nil
}
40 changes: 40 additions & 0 deletions rules/en/timezone_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package en

import (
"testing"
"time"

"github.com/olebedev/when/rules"
"github.com/stretchr/testify/assert"
)

func TestTimezone(t *testing.T) {
rule := Timezone(rules.Override)
ref := time.Date(2026, 10, 14, 12, 0, 0, 0, time.UTC)

tests := []struct {
text string
expectedZone string
matched bool
}{
{"10 PM EST", "EST", true},
{"10:30am PDT", "PDT", true},
{"tomorrow at 5pm UTC", "UTC", true},
{"12:00 +02:00", "+02:00", true},
{"invalid", "", false},
}

for _, tc := range tests {
m := rule.Find(tc.text)
if !tc.matched {
assert.Nil(t, m)
continue
}
assert.NotNil(t, m)
ctx := &rules.Context{}
ok, err := m.Apply(ctx, nil, ref)
assert.NoError(t, err)
assert.True(t, ok)
assert.NotNil(t, ctx.Location)
}
}
Loading