feat(rules): add an opt-in timezone rule (#23)
common.Timezone matches UTC and GMT with an optional offset (UTC+2, GMT-05:30, GMT+0530) and a small table of upper case abbreviations (EST, PDT, CET, ...), and sets Context.Location, so "10 pm EST" comes back as 22:00 in a -05:00 zone. A timezone alone does not count as a match. It is not added to common.All, so existing parsers behave as before.
This commit is contained in:
1 parent
c0f903d615
commit
fa0fa5b55b
3 files changed
+138
No files matched your search
@@ -103,6 +103,16 @@ fmt.Printf(r.Time.String())
|
||||
// 13:46:21 (correct)
|
||||
```
|
||||
|
||||
#### Timezones
|
||||
|
||||
The timezone rule is not part of `common.All`, add it to have `10 pm EST` or `22:00 UTC+2` returned in that zone:
|
||||
|
||||
```go
|
||||
w.Add(common.Timezone(rules.Override))
|
||||
```
|
||||
|
||||
It knows `UTC` and `GMT` with an optional offset (`GMT-05:30`, `UTC+0530`) and a few upper case abbreviations (`EST`, `PDT`, `CET`, ...), each as a fixed offset.
|
||||
|
||||
### State of the project
|
||||
|
||||
The project is in a more-or-less complete state. It's used for one project already. Bugs will be fixed as soon as they will be found.
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/olebedev/when/rules"
|
||||
)
|
||||
|
||||
/*
|
||||
- 10 pm EST
|
||||
- 22:00 UTC
|
||||
- 9am GMT+2
|
||||
- 18:30 utc-05:00
|
||||
- 18:30 GMT+0530
|
||||
|
||||
Sets the location of the time found by the other rules. A timezone on its
|
||||
own is not a time, so the rule never yields a result by itself.
|
||||
|
||||
Abbreviations are matched in upper case only, so "est" in a sentence is
|
||||
not taken for a timezone, and each one is a fixed offset from UTC.
|
||||
*/
|
||||
|
||||
var timezoneOffsets = map[string]time.Duration{
|
||||
"HST": -10 * time.Hour,
|
||||
"AKST": -9 * time.Hour,
|
||||
"AKDT": -8 * time.Hour,
|
||||
"PST": -8 * time.Hour,
|
||||
"PDT": -7 * time.Hour,
|
||||
"MST": -7 * time.Hour,
|
||||
"MDT": -6 * time.Hour,
|
||||
"CST": -6 * time.Hour,
|
||||
"CDT": -5 * time.Hour,
|
||||
"EST": -5 * time.Hour,
|
||||
"EDT": -4 * time.Hour,
|
||||
"BST": 1 * time.Hour,
|
||||
"CET": 1 * time.Hour,
|
||||
"CEST": 2 * time.Hour,
|
||||
"EET": 2 * time.Hour,
|
||||
"EEST": 3 * time.Hour,
|
||||
"JST": 9 * time.Hour,
|
||||
"AEST": 10 * time.Hour,
|
||||
"AEDT": 11 * time.Hour,
|
||||
}
|
||||
|
||||
func Timezone(s rules.Strategy) rules.Rule {
|
||||
abbreviations := make([]string, 0, len(timezoneOffsets))
|
||||
for abbreviation := range timezoneOffsets {
|
||||
abbreviations = append(abbreviations, abbreviation)
|
||||
}
|
||||
sort.Strings(abbreviations)
|
||||
|
||||
return &rules.F{
|
||||
RegExp: regexp.MustCompile("(?:\\W|^)" +
|
||||
"((?i:utc|gmt)(?:([+-])(\\d{1,2})(?::?(\\d{2}))?)?|" +
|
||||
strings.Join(abbreviations, "|") + ")" +
|
||||
"(?:\\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
|
||||
}
|
||||
|
||||
name := strings.ToUpper(m.Captures[0])
|
||||
offset, ok := timezoneOffsets[name]
|
||||
if !ok {
|
||||
hours, _ := strconv.Atoi(m.Captures[2])
|
||||
minutes, _ := strconv.Atoi(m.Captures[3])
|
||||
if hours > 14 || minutes > 59 {
|
||||
return false, nil
|
||||
}
|
||||
offset = time.Duration(hours)*time.Hour + time.Duration(minutes)*time.Minute
|
||||
if m.Captures[1] == "-" {
|
||||
offset = -offset
|
||||
}
|
||||
}
|
||||
|
||||
c.Location = time.FixedZone(name, int(offset.Seconds()))
|
||||
return false, nil
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package common_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/olebedev/when"
|
||||
"github.com/olebedev/when/rules"
|
||||
"github.com/olebedev/when/rules/common"
|
||||
"github.com/olebedev/when/rules/en"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestTimezone(t *testing.T) {
|
||||
fixt := []Fixture{
|
||||
{"call me at 10 pm EST", 11, "10 pm EST", 27 * time.Hour},
|
||||
{"call me at 10 pm PDT", 11, "10 pm PDT", 29 * time.Hour},
|
||||
{"call me at 10 pm CEST", 11, "10 pm CEST", 20 * time.Hour},
|
||||
{"at 22:00 UTC", 3, "22:00 UTC", 22 * time.Hour},
|
||||
{"at 22:00 utc+2", 3, "22:00 utc+2", 20 * time.Hour},
|
||||
{"at 22:00 GMT-05:30", 3, "22:00 GMT-05:30", 27*time.Hour + 30*time.Minute},
|
||||
{"at 22:00 GMT+0530", 3, "22:00 GMT+0530", 16*time.Hour + 30*time.Minute},
|
||||
|
||||
// abbreviations only in upper case, "est" is left alone
|
||||
{"call me at 10 pm est", 11, "10 pm", 22 * time.Hour},
|
||||
}
|
||||
|
||||
w := when.New(nil)
|
||||
w.Add(en.All...)
|
||||
w.Add(common.Timezone(rules.Override))
|
||||
|
||||
ApplyFixtures(t, "common.Timezone", w, fixt)
|
||||
|
||||
nilFixt := []Fixture{
|
||||
{"the EST office", 0, "a timezone alone is not a time", 0},
|
||||
}
|
||||
ApplyFixturesNil(t, "common.Timezone nil", w, nilFixt)
|
||||
|
||||
res, err := w.Parse("call me at 10 pm EST", null)
|
||||
require.Nil(t, err)
|
||||
name, offset := res.Time.Zone()
|
||||
require.Equal(t, "EST", name)
|
||||
require.Equal(t, -5*60*60, offset)
|
||||
}
|
||||
Reference in new issue
Block a user