alertmanager/timeinterval/timeinterval.go

605 lines
17 KiB
Go
Raw Normal View History

// Copyright 2020 Prometheus Team
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package timeinterval
import (
"encoding/json"
"errors"
"fmt"
Add timezone support to time intervals. (#2782) * Add explicit UTC to time interval tests Signed-off-by: Ben Ridley <benridley29@gmail.com> * Add timezone support to time intervals Signed-off-by: Ben Ridley <benridley29@gmail.com> * Update time interval documentation with time zone info Signed-off-by: Ben Ridley <benridley29@gmail.com> * Refactor notification tests to test timezone support Signed-off-by: Ben Ridley <benridley29@gmail.com> * Make use of Local more clear Signed-off-by: Ben Ridley <benridley29@gmail.com> * Fix documentation about timezone support. Makes it clear that the default is UTC, but others are supported. Signed-off-by: Ben Ridley <benridley29@gmail.com> * Remove commented/unused function Signed-off-by: Ben Ridley <benridley29@gmail.com> * Fix tests using incorrect timezones Previously tests were using time zone names that were unsupported by the RFC822 parser. This switches the tests to use RFC822Z and specifies the zones by number. Signed-off-by: Ben Ridley <benridley29@gmail.com> * Add a few more timezone test cases Signed-off-by: Ben Ridley <benridley29@gmail.com> * Remove unnecessary if/else branch Co-authored-by: Sylvain Rabot <sylvain@abstraction.fr> Signed-off-by: Ben Ridley <benridley29@gmail.com> * Rename timezone to location for consistency with Go stdlib Signed-off-by: Ben Ridley <benridley29@gmail.com> * Make Windows timezone error more specific Signed-off-by: Ben Ridley <benridley29@gmail.com> * Update docs to use 'location' Signed-off-by: Ben Ridley <benridley29@gmail.com> * Apply suggestions from code review Co-authored-by: Sylvain Rabot <sylvain@abstraction.fr> Signed-off-by: Ben Ridley <benridley29@gmail.com> Signed-off-by: Ben Ridley <benridley29@gmail.com> Co-authored-by: Sylvain Rabot <sylvain@abstraction.fr>
2022-09-22 12:45:17 +00:00
"os"
"regexp"
Add timezone support to time intervals. (#2782) * Add explicit UTC to time interval tests Signed-off-by: Ben Ridley <benridley29@gmail.com> * Add timezone support to time intervals Signed-off-by: Ben Ridley <benridley29@gmail.com> * Update time interval documentation with time zone info Signed-off-by: Ben Ridley <benridley29@gmail.com> * Refactor notification tests to test timezone support Signed-off-by: Ben Ridley <benridley29@gmail.com> * Make use of Local more clear Signed-off-by: Ben Ridley <benridley29@gmail.com> * Fix documentation about timezone support. Makes it clear that the default is UTC, but others are supported. Signed-off-by: Ben Ridley <benridley29@gmail.com> * Remove commented/unused function Signed-off-by: Ben Ridley <benridley29@gmail.com> * Fix tests using incorrect timezones Previously tests were using time zone names that were unsupported by the RFC822 parser. This switches the tests to use RFC822Z and specifies the zones by number. Signed-off-by: Ben Ridley <benridley29@gmail.com> * Add a few more timezone test cases Signed-off-by: Ben Ridley <benridley29@gmail.com> * Remove unnecessary if/else branch Co-authored-by: Sylvain Rabot <sylvain@abstraction.fr> Signed-off-by: Ben Ridley <benridley29@gmail.com> * Rename timezone to location for consistency with Go stdlib Signed-off-by: Ben Ridley <benridley29@gmail.com> * Make Windows timezone error more specific Signed-off-by: Ben Ridley <benridley29@gmail.com> * Update docs to use 'location' Signed-off-by: Ben Ridley <benridley29@gmail.com> * Apply suggestions from code review Co-authored-by: Sylvain Rabot <sylvain@abstraction.fr> Signed-off-by: Ben Ridley <benridley29@gmail.com> Signed-off-by: Ben Ridley <benridley29@gmail.com> Co-authored-by: Sylvain Rabot <sylvain@abstraction.fr>
2022-09-22 12:45:17 +00:00
"runtime"
"strconv"
"strings"
"time"
"gopkg.in/yaml.v2"
)
// TimeInterval describes intervals of time. ContainsTime will tell you if a golang time is contained
// within the interval.
type TimeInterval struct {
Times []TimeRange `yaml:"times,omitempty" json:"times,omitempty"`
Weekdays []WeekdayRange `yaml:"weekdays,flow,omitempty" json:"weekdays,omitempty"`
DaysOfMonth []DayOfMonthRange `yaml:"days_of_month,flow,omitempty" json:"days_of_month,omitempty"`
Months []MonthRange `yaml:"months,flow,omitempty" json:"months,omitempty"`
Years []YearRange `yaml:"years,flow,omitempty" json:"years,omitempty"`
Add timezone support to time intervals. (#2782) * Add explicit UTC to time interval tests Signed-off-by: Ben Ridley <benridley29@gmail.com> * Add timezone support to time intervals Signed-off-by: Ben Ridley <benridley29@gmail.com> * Update time interval documentation with time zone info Signed-off-by: Ben Ridley <benridley29@gmail.com> * Refactor notification tests to test timezone support Signed-off-by: Ben Ridley <benridley29@gmail.com> * Make use of Local more clear Signed-off-by: Ben Ridley <benridley29@gmail.com> * Fix documentation about timezone support. Makes it clear that the default is UTC, but others are supported. Signed-off-by: Ben Ridley <benridley29@gmail.com> * Remove commented/unused function Signed-off-by: Ben Ridley <benridley29@gmail.com> * Fix tests using incorrect timezones Previously tests were using time zone names that were unsupported by the RFC822 parser. This switches the tests to use RFC822Z and specifies the zones by number. Signed-off-by: Ben Ridley <benridley29@gmail.com> * Add a few more timezone test cases Signed-off-by: Ben Ridley <benridley29@gmail.com> * Remove unnecessary if/else branch Co-authored-by: Sylvain Rabot <sylvain@abstraction.fr> Signed-off-by: Ben Ridley <benridley29@gmail.com> * Rename timezone to location for consistency with Go stdlib Signed-off-by: Ben Ridley <benridley29@gmail.com> * Make Windows timezone error more specific Signed-off-by: Ben Ridley <benridley29@gmail.com> * Update docs to use 'location' Signed-off-by: Ben Ridley <benridley29@gmail.com> * Apply suggestions from code review Co-authored-by: Sylvain Rabot <sylvain@abstraction.fr> Signed-off-by: Ben Ridley <benridley29@gmail.com> Signed-off-by: Ben Ridley <benridley29@gmail.com> Co-authored-by: Sylvain Rabot <sylvain@abstraction.fr>
2022-09-22 12:45:17 +00:00
Location *Location `yaml:"location,flow,omitempty" json:"location,omitempty"`
}
// TimeRange represents a range of minutes within a 1440 minute day, exclusive of the End minute. A day consists of 1440 minutes.
// For example, 4:00PM to End of the day would Begin at 1020 and End at 1440.
type TimeRange struct {
StartMinute int
EndMinute int
}
// InclusiveRange is used to hold the Beginning and End values of many time interval components.
type InclusiveRange struct {
Begin int
End int
}
// A WeekdayRange is an inclusive range between [0, 6] where 0 = Sunday.
type WeekdayRange struct {
InclusiveRange
}
// A DayOfMonthRange is an inclusive range that may have negative Beginning/End values that represent distance from the End of the month Beginning at -1.
type DayOfMonthRange struct {
InclusiveRange
}
// A MonthRange is an inclusive range between [1, 12] where 1 = January.
type MonthRange struct {
InclusiveRange
}
// A YearRange is a positive inclusive range.
type YearRange struct {
InclusiveRange
}
Add timezone support to time intervals. (#2782) * Add explicit UTC to time interval tests Signed-off-by: Ben Ridley <benridley29@gmail.com> * Add timezone support to time intervals Signed-off-by: Ben Ridley <benridley29@gmail.com> * Update time interval documentation with time zone info Signed-off-by: Ben Ridley <benridley29@gmail.com> * Refactor notification tests to test timezone support Signed-off-by: Ben Ridley <benridley29@gmail.com> * Make use of Local more clear Signed-off-by: Ben Ridley <benridley29@gmail.com> * Fix documentation about timezone support. Makes it clear that the default is UTC, but others are supported. Signed-off-by: Ben Ridley <benridley29@gmail.com> * Remove commented/unused function Signed-off-by: Ben Ridley <benridley29@gmail.com> * Fix tests using incorrect timezones Previously tests were using time zone names that were unsupported by the RFC822 parser. This switches the tests to use RFC822Z and specifies the zones by number. Signed-off-by: Ben Ridley <benridley29@gmail.com> * Add a few more timezone test cases Signed-off-by: Ben Ridley <benridley29@gmail.com> * Remove unnecessary if/else branch Co-authored-by: Sylvain Rabot <sylvain@abstraction.fr> Signed-off-by: Ben Ridley <benridley29@gmail.com> * Rename timezone to location for consistency with Go stdlib Signed-off-by: Ben Ridley <benridley29@gmail.com> * Make Windows timezone error more specific Signed-off-by: Ben Ridley <benridley29@gmail.com> * Update docs to use 'location' Signed-off-by: Ben Ridley <benridley29@gmail.com> * Apply suggestions from code review Co-authored-by: Sylvain Rabot <sylvain@abstraction.fr> Signed-off-by: Ben Ridley <benridley29@gmail.com> Signed-off-by: Ben Ridley <benridley29@gmail.com> Co-authored-by: Sylvain Rabot <sylvain@abstraction.fr>
2022-09-22 12:45:17 +00:00
// A Location is a container for a time.Location, used for custom unmarshalling/validation logic.
type Location struct {
*time.Location
}
type yamlTimeRange struct {
StartTime string `yaml:"start_time" json:"start_time"`
EndTime string `yaml:"end_time" json:"end_time"`
}
// A range with a Beginning and End that can be represented as strings.
type stringableRange interface {
setBegin(int)
setEnd(int)
// Try to map a member of the range into an integer.
memberFromString(string) (int, error)
}
func (ir *InclusiveRange) setBegin(n int) {
ir.Begin = n
}
func (ir *InclusiveRange) setEnd(n int) {
ir.End = n
}
func (ir *InclusiveRange) memberFromString(in string) (out int, err error) {
out, err = strconv.Atoi(in)
if err != nil {
return -1, err
}
return out, nil
}
func (r *WeekdayRange) memberFromString(in string) (out int, err error) {
out, ok := daysOfWeek[in]
if !ok {
return -1, fmt.Errorf("%s is not a valid weekday", in)
}
return out, nil
}
func (r *MonthRange) memberFromString(in string) (out int, err error) {
out, ok := months[in]
if !ok {
out, err = strconv.Atoi(in)
if err != nil {
return -1, fmt.Errorf("%s is not a valid month", in)
}
}
return out, nil
}
var daysOfWeek = map[string]int{
"sunday": 0,
"monday": 1,
"tuesday": 2,
"wednesday": 3,
"thursday": 4,
"friday": 5,
"saturday": 6,
}
var daysOfWeekInv = map[int]string{
0: "sunday",
1: "monday",
2: "tuesday",
3: "wednesday",
4: "thursday",
5: "friday",
6: "saturday",
}
var months = map[string]int{
"january": 1,
"february": 2,
"march": 3,
"april": 4,
"may": 5,
"june": 6,
"july": 7,
"august": 8,
"september": 9,
"october": 10,
"november": 11,
"december": 12,
}
var monthsInv = map[int]string{
1: "january",
2: "february",
3: "march",
4: "april",
5: "may",
6: "june",
7: "july",
8: "august",
9: "september",
10: "october",
11: "november",
12: "december",
}
Add timezone support to time intervals. (#2782) * Add explicit UTC to time interval tests Signed-off-by: Ben Ridley <benridley29@gmail.com> * Add timezone support to time intervals Signed-off-by: Ben Ridley <benridley29@gmail.com> * Update time interval documentation with time zone info Signed-off-by: Ben Ridley <benridley29@gmail.com> * Refactor notification tests to test timezone support Signed-off-by: Ben Ridley <benridley29@gmail.com> * Make use of Local more clear Signed-off-by: Ben Ridley <benridley29@gmail.com> * Fix documentation about timezone support. Makes it clear that the default is UTC, but others are supported. Signed-off-by: Ben Ridley <benridley29@gmail.com> * Remove commented/unused function Signed-off-by: Ben Ridley <benridley29@gmail.com> * Fix tests using incorrect timezones Previously tests were using time zone names that were unsupported by the RFC822 parser. This switches the tests to use RFC822Z and specifies the zones by number. Signed-off-by: Ben Ridley <benridley29@gmail.com> * Add a few more timezone test cases Signed-off-by: Ben Ridley <benridley29@gmail.com> * Remove unnecessary if/else branch Co-authored-by: Sylvain Rabot <sylvain@abstraction.fr> Signed-off-by: Ben Ridley <benridley29@gmail.com> * Rename timezone to location for consistency with Go stdlib Signed-off-by: Ben Ridley <benridley29@gmail.com> * Make Windows timezone error more specific Signed-off-by: Ben Ridley <benridley29@gmail.com> * Update docs to use 'location' Signed-off-by: Ben Ridley <benridley29@gmail.com> * Apply suggestions from code review Co-authored-by: Sylvain Rabot <sylvain@abstraction.fr> Signed-off-by: Ben Ridley <benridley29@gmail.com> Signed-off-by: Ben Ridley <benridley29@gmail.com> Co-authored-by: Sylvain Rabot <sylvain@abstraction.fr>
2022-09-22 12:45:17 +00:00
// UnmarshalYAML implements the Unmarshaller interface for Location.
func (tz *Location) UnmarshalYAML(unmarshal func(interface{}) error) error {
var str string
if err := unmarshal(&str); err != nil {
return err
}
loc, err := time.LoadLocation(str)
if err != nil {
if runtime.GOOS == "windows" {
if zoneinfo := os.Getenv("ZONEINFO"); zoneinfo != "" {
return fmt.Errorf("%w (ZONEINFO=%q)", err, zoneinfo)
}
return fmt.Errorf("%w (on Windows platforms, you may have to pass the time zone database using the ZONEINFO environment variable, see https://pkg.go.dev/time#LoadLocation for details)", err)
}
return err
}
*tz = Location{loc}
return nil
}
// UnmarshalJSON implements the json.Unmarshaler interface for Location.
// It delegates to the YAML unmarshaller as it can parse JSON and has validation logic.
func (tz *Location) UnmarshalJSON(in []byte) error {
return yaml.Unmarshal(in, tz)
}
// UnmarshalYAML implements the Unmarshaller interface for WeekdayRange.
func (r *WeekdayRange) UnmarshalYAML(unmarshal func(interface{}) error) error {
var str string
if err := unmarshal(&str); err != nil {
return err
}
if err := stringableRangeFromString(str, r); err != nil {
return err
}
if r.Begin > r.End {
return errors.New("start day cannot be before end day")
}
if r.Begin < 0 || r.Begin > 6 {
return fmt.Errorf("%s is not a valid day of the week: out of range", str)
}
if r.End < 0 || r.End > 6 {
return fmt.Errorf("%s is not a valid day of the week: out of range", str)
}
return nil
}
// UnmarshalJSON implements the json.Unmarshaler interface for WeekdayRange.
// It delegates to the YAML unmarshaller as it can parse JSON and has validation logic.
func (r *WeekdayRange) UnmarshalJSON(in []byte) error {
return yaml.Unmarshal(in, r)
}
// UnmarshalYAML implements the Unmarshaller interface for DayOfMonthRange.
func (r *DayOfMonthRange) UnmarshalYAML(unmarshal func(interface{}) error) error {
var str string
if err := unmarshal(&str); err != nil {
return err
}
if err := stringableRangeFromString(str, r); err != nil {
return err
}
// Check beginning <= end accounting for negatives day of month indices as well.
// Months != 31 days can't be addressed here and are clamped, but at least we can catch blatant errors.
if r.Begin == 0 || r.Begin < -31 || r.Begin > 31 {
return fmt.Errorf("%d is not a valid day of the month: out of range", r.Begin)
}
if r.End == 0 || r.End < -31 || r.End > 31 {
return fmt.Errorf("%d is not a valid day of the month: out of range", r.End)
}
// Restricting here prevents errors where begin > end in longer months but not shorter months.
if r.Begin < 0 && r.End > 0 {
return fmt.Errorf("end day must be negative if start day is negative")
}
// Check begin <= end. We can't know this for sure when using negative indices
// but we can prevent cases where its always invalid (using 28 day minimum length).
checkBegin := r.Begin
checkEnd := r.End
if r.Begin < 0 {
checkBegin = 28 + r.Begin
}
if r.End < 0 {
checkEnd = 28 + r.End
}
if checkBegin > checkEnd {
return fmt.Errorf("end day %d is always before start day %d", r.End, r.Begin)
}
return nil
}
// UnmarshalJSON implements the json.Unmarshaler interface for DayOfMonthRange.
// It delegates to the YAML unmarshaller as it can parse JSON and has validation logic.
func (r *DayOfMonthRange) UnmarshalJSON(in []byte) error {
return yaml.Unmarshal(in, r)
}
// UnmarshalYAML implements the Unmarshaller interface for MonthRange.
func (r *MonthRange) UnmarshalYAML(unmarshal func(interface{}) error) error {
var str string
if err := unmarshal(&str); err != nil {
return err
}
if err := stringableRangeFromString(str, r); err != nil {
return err
}
if r.Begin > r.End {
begin := monthsInv[r.Begin]
end := monthsInv[r.End]
return fmt.Errorf("end month %s is before start month %s", end, begin)
}
return nil
}
// UnmarshalJSON implements the json.Unmarshaler interface for MonthRange.
// It delegates to the YAML unmarshaller as it can parse JSON and has validation logic.
func (r *MonthRange) UnmarshalJSON(in []byte) error {
return yaml.Unmarshal(in, r)
}
// UnmarshalYAML implements the Unmarshaller interface for YearRange.
func (r *YearRange) UnmarshalYAML(unmarshal func(interface{}) error) error {
var str string
if err := unmarshal(&str); err != nil {
return err
}
if err := stringableRangeFromString(str, r); err != nil {
return err
}
if r.Begin > r.End {
return fmt.Errorf("end year %d is before start year %d", r.End, r.Begin)
}
return nil
}
// UnmarshalJSON implements the json.Unmarshaler interface for YearRange.
// It delegates to the YAML unmarshaller as it can parse JSON and has validation logic.
func (r *YearRange) UnmarshalJSON(in []byte) error {
return yaml.Unmarshal(in, r)
}
// UnmarshalYAML implements the Unmarshaller interface for TimeRanges.
func (tr *TimeRange) UnmarshalYAML(unmarshal func(interface{}) error) error {
var y yamlTimeRange
if err := unmarshal(&y); err != nil {
return err
}
if y.EndTime == "" || y.StartTime == "" {
return errors.New("both start and end times must be provided")
}
start, err := parseTime(y.StartTime)
if err != nil {
return err
}
end, err := parseTime(y.EndTime)
if err != nil {
return err
}
if start >= end {
return errors.New("start time cannot be equal or greater than end time")
}
tr.StartMinute, tr.EndMinute = start, end
return nil
}
// UnmarshalJSON implements the json.Unmarshaler interface for Timerange.
// It delegates to the YAML unmarshaller as it can parse JSON and has validation logic.
func (tr *TimeRange) UnmarshalJSON(in []byte) error {
return yaml.Unmarshal(in, tr)
}
// MarshalYAML implements the yaml.Marshaler interface for WeekdayRange.
func (r WeekdayRange) MarshalYAML() (interface{}, error) {
bytes, err := r.MarshalText()
return string(bytes), err
}
// MarshalText implements the econding.TextMarshaler interface for WeekdayRange.
// It converts the range into a colon-separated string, or a single weekday if possible.
// e.g. "monday:friday" or "saturday".
func (r WeekdayRange) MarshalText() ([]byte, error) {
beginStr, ok := daysOfWeekInv[r.Begin]
if !ok {
return nil, fmt.Errorf("unable to convert %d into weekday string", r.Begin)
}
if r.Begin == r.End {
return []byte(beginStr), nil
}
endStr, ok := daysOfWeekInv[r.End]
if !ok {
return nil, fmt.Errorf("unable to convert %d into weekday string", r.End)
}
rangeStr := fmt.Sprintf("%s:%s", beginStr, endStr)
return []byte(rangeStr), nil
}
// MarshalYAML implements the yaml.Marshaler interface for TimeRange.
func (tr TimeRange) MarshalYAML() (out interface{}, err error) {
startHr := tr.StartMinute / 60
endHr := tr.EndMinute / 60
startMin := tr.StartMinute % 60
endMin := tr.EndMinute % 60
startStr := fmt.Sprintf("%02d:%02d", startHr, startMin)
endStr := fmt.Sprintf("%02d:%02d", endHr, endMin)
yTr := yamlTimeRange{startStr, endStr}
return interface{}(yTr), err
}
// MarshalJSON implements the json.Marshaler interface for TimeRange.
func (tr TimeRange) MarshalJSON() (out []byte, err error) {
startHr := tr.StartMinute / 60
endHr := tr.EndMinute / 60
startMin := tr.StartMinute % 60
endMin := tr.EndMinute % 60
startStr := fmt.Sprintf("%02d:%02d", startHr, startMin)
endStr := fmt.Sprintf("%02d:%02d", endHr, endMin)
yTr := yamlTimeRange{startStr, endStr}
return json.Marshal(yTr)
}
Add timezone support to time intervals. (#2782) * Add explicit UTC to time interval tests Signed-off-by: Ben Ridley <benridley29@gmail.com> * Add timezone support to time intervals Signed-off-by: Ben Ridley <benridley29@gmail.com> * Update time interval documentation with time zone info Signed-off-by: Ben Ridley <benridley29@gmail.com> * Refactor notification tests to test timezone support Signed-off-by: Ben Ridley <benridley29@gmail.com> * Make use of Local more clear Signed-off-by: Ben Ridley <benridley29@gmail.com> * Fix documentation about timezone support. Makes it clear that the default is UTC, but others are supported. Signed-off-by: Ben Ridley <benridley29@gmail.com> * Remove commented/unused function Signed-off-by: Ben Ridley <benridley29@gmail.com> * Fix tests using incorrect timezones Previously tests were using time zone names that were unsupported by the RFC822 parser. This switches the tests to use RFC822Z and specifies the zones by number. Signed-off-by: Ben Ridley <benridley29@gmail.com> * Add a few more timezone test cases Signed-off-by: Ben Ridley <benridley29@gmail.com> * Remove unnecessary if/else branch Co-authored-by: Sylvain Rabot <sylvain@abstraction.fr> Signed-off-by: Ben Ridley <benridley29@gmail.com> * Rename timezone to location for consistency with Go stdlib Signed-off-by: Ben Ridley <benridley29@gmail.com> * Make Windows timezone error more specific Signed-off-by: Ben Ridley <benridley29@gmail.com> * Update docs to use 'location' Signed-off-by: Ben Ridley <benridley29@gmail.com> * Apply suggestions from code review Co-authored-by: Sylvain Rabot <sylvain@abstraction.fr> Signed-off-by: Ben Ridley <benridley29@gmail.com> Signed-off-by: Ben Ridley <benridley29@gmail.com> Co-authored-by: Sylvain Rabot <sylvain@abstraction.fr>
2022-09-22 12:45:17 +00:00
// MarshalText implements the econding.TextMarshaler interface for Location.
// It marshals a Location back into a string that represents a time.Location.
func (tz Location) MarshalText() ([]byte, error) {
if tz.Location == nil {
return nil, fmt.Errorf("unable to convert nil location into string")
}
return []byte(tz.Location.String()), nil
}
//MarshalYAML implements the yaml.Marshaler interface for Location.
func (tz Location) MarshalYAML() (interface{}, error) {
bytes, err := tz.MarshalText()
return string(bytes), err
}
//MarshalJSON implements the json.Marshaler interface for Location.
func (tz Location) MarshalJSON() (out []byte, err error) {
return json.Marshal(tz.String())
}
// MarshalText implements the encoding.TextMarshaler interface for InclusiveRange.
// It converts the struct into a colon-separated string, or a single element if
// appropriate. e.g. "monday:friday" or "monday"
func (ir InclusiveRange) MarshalText() ([]byte, error) {
if ir.Begin == ir.End {
return []byte(strconv.Itoa(ir.Begin)), nil
}
out := fmt.Sprintf("%d:%d", ir.Begin, ir.End)
return []byte(out), nil
}
// MarshalYAML implements the yaml.Marshaler interface for InclusiveRange.
func (ir InclusiveRange) MarshalYAML() (interface{}, error) {
bytes, err := ir.MarshalText()
return string(bytes), err
}
// TimeLayout specifies the layout to be used in time.Parse() calls for time intervals.
const TimeLayout = "15:04"
var (
validTime = "^((([01][0-9])|(2[0-3])):[0-5][0-9])$|(^24:00$)"
validTimeRE = regexp.MustCompile(validTime)
)
// Given a time, determines the number of days in the month that time occurs in.
func daysInMonth(t time.Time) int {
monthStart := time.Date(t.Year(), t.Month(), 1, 0, 0, 0, 0, t.Location())
monthEnd := monthStart.AddDate(0, 1, 0)
diff := monthEnd.Sub(monthStart)
return int(diff.Hours() / 24)
}
func clamp(n, min, max int) int {
if n <= min {
return min
}
if n >= max {
return max
}
return n
}
// ContainsTime returns true if the TimeInterval contains the given time, otherwise returns false.
func (tp TimeInterval) ContainsTime(t time.Time) bool {
Add timezone support to time intervals. (#2782) * Add explicit UTC to time interval tests Signed-off-by: Ben Ridley <benridley29@gmail.com> * Add timezone support to time intervals Signed-off-by: Ben Ridley <benridley29@gmail.com> * Update time interval documentation with time zone info Signed-off-by: Ben Ridley <benridley29@gmail.com> * Refactor notification tests to test timezone support Signed-off-by: Ben Ridley <benridley29@gmail.com> * Make use of Local more clear Signed-off-by: Ben Ridley <benridley29@gmail.com> * Fix documentation about timezone support. Makes it clear that the default is UTC, but others are supported. Signed-off-by: Ben Ridley <benridley29@gmail.com> * Remove commented/unused function Signed-off-by: Ben Ridley <benridley29@gmail.com> * Fix tests using incorrect timezones Previously tests were using time zone names that were unsupported by the RFC822 parser. This switches the tests to use RFC822Z and specifies the zones by number. Signed-off-by: Ben Ridley <benridley29@gmail.com> * Add a few more timezone test cases Signed-off-by: Ben Ridley <benridley29@gmail.com> * Remove unnecessary if/else branch Co-authored-by: Sylvain Rabot <sylvain@abstraction.fr> Signed-off-by: Ben Ridley <benridley29@gmail.com> * Rename timezone to location for consistency with Go stdlib Signed-off-by: Ben Ridley <benridley29@gmail.com> * Make Windows timezone error more specific Signed-off-by: Ben Ridley <benridley29@gmail.com> * Update docs to use 'location' Signed-off-by: Ben Ridley <benridley29@gmail.com> * Apply suggestions from code review Co-authored-by: Sylvain Rabot <sylvain@abstraction.fr> Signed-off-by: Ben Ridley <benridley29@gmail.com> Signed-off-by: Ben Ridley <benridley29@gmail.com> Co-authored-by: Sylvain Rabot <sylvain@abstraction.fr>
2022-09-22 12:45:17 +00:00
if tp.Location != nil {
t = t.In(tp.Location.Location)
}
if tp.Times != nil {
in := false
for _, validMinutes := range tp.Times {
if (t.Hour()*60+t.Minute()) >= validMinutes.StartMinute && (t.Hour()*60+t.Minute()) < validMinutes.EndMinute {
in = true
break
}
}
if !in {
return false
}
}
if tp.DaysOfMonth != nil {
in := false
for _, validDates := range tp.DaysOfMonth {
var begin, end int
daysInMonth := daysInMonth(t)
if validDates.Begin < 0 {
begin = daysInMonth + validDates.Begin + 1
} else {
begin = validDates.Begin
}
if validDates.End < 0 {
end = daysInMonth + validDates.End + 1
} else {
end = validDates.End
}
// Skip clamping if the beginning date is after the end of the month.
if begin > daysInMonth {
continue
}
// Clamp to the boundaries of the month to prevent crossing into other months.
begin = clamp(begin, -1*daysInMonth, daysInMonth)
end = clamp(end, -1*daysInMonth, daysInMonth)
if t.Day() >= begin && t.Day() <= end {
in = true
break
}
}
if !in {
return false
}
}
if tp.Months != nil {
in := false
for _, validMonths := range tp.Months {
if t.Month() >= time.Month(validMonths.Begin) && t.Month() <= time.Month(validMonths.End) {
in = true
break
}
}
if !in {
return false
}
}
if tp.Weekdays != nil {
in := false
for _, validDays := range tp.Weekdays {
if t.Weekday() >= time.Weekday(validDays.Begin) && t.Weekday() <= time.Weekday(validDays.End) {
in = true
break
}
}
if !in {
return false
}
}
if tp.Years != nil {
in := false
for _, validYears := range tp.Years {
if t.Year() >= validYears.Begin && t.Year() <= validYears.End {
in = true
break
}
}
if !in {
return false
}
}
return true
}
// Converts a string of the form "HH:MM" into the number of minutes elapsed in the day.
func parseTime(in string) (mins int, err error) {
if !validTimeRE.MatchString(in) {
return 0, fmt.Errorf("couldn't parse timestamp %s, invalid format", in)
}
timestampComponents := strings.Split(in, ":")
if len(timestampComponents) != 2 {
return 0, fmt.Errorf("invalid timestamp format: %s", in)
}
timeStampHours, err := strconv.Atoi(timestampComponents[0])
if err != nil {
return 0, err
}
timeStampMinutes, err := strconv.Atoi(timestampComponents[1])
if err != nil {
return 0, err
}
if timeStampHours < 0 || timeStampHours > 24 || timeStampMinutes < 0 || timeStampMinutes > 60 {
return 0, fmt.Errorf("timestamp %s out of range", in)
}
// Timestamps are stored as minutes elapsed in the day, so multiply hours by 60.
mins = timeStampHours*60 + timeStampMinutes
return mins, nil
}
// Converts a range that can be represented as strings (e.g. monday:wednesday) into an equivalent integer-represented range.
func stringableRangeFromString(in string, r stringableRange) (err error) {
in = strings.ToLower(in)
if strings.ContainsRune(in, ':') {
components := strings.Split(in, ":")
if len(components) != 2 {
return fmt.Errorf("couldn't parse range %s, invalid format", in)
}
start, err := r.memberFromString(components[0])
if err != nil {
return err
}
End, err := r.memberFromString(components[1])
if err != nil {
return err
}
r.setBegin(start)
r.setEnd(End)
return nil
}
val, err := r.memberFromString(in)
if err != nil {
return err
}
r.setBegin(val)
r.setEnd(val)
return nil
}