Skip to content

Commit

Permalink
enhance: time/timeutil: support Month in ParseTimeRangeInterval()
Browse files Browse the repository at this point in the history
  • Loading branch information
grokify committed Dec 9, 2023
1 parent 028f1d7 commit d3e4c30
Show file tree
Hide file tree
Showing 2 changed files with 76 additions and 34 deletions.
82 changes: 48 additions & 34 deletions time/timeutil/time_range.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,9 @@ type TimeRange struct {
MaxSet bool
}

var rxParseTimeRange = regexp.MustCompile(`^([0-9]+)([MQHY])([0-9]+)$`)
var rxParseTimeRange = regexp.MustCompile(`^([0-9]+)([MQH])([0-9]+)$`)

// ParseTimeRangeInterval takes a string in the form of `YYYY[MQY]XX`.
// ParseTimeRangeInterval takes a string in the form of `YYYY[MQHX]XX`.
func ParseTimeRangeInterval(s string) (TimeRange, error) {
s1 := strings.ToUpper(strings.TrimSpace(s))
m := rxParseTimeRange.FindStringSubmatch(s1)
Expand All @@ -76,56 +76,70 @@ func ParseTimeRangeInterval(s string) (TimeRange, error) {
switch m[2] {
case "H":
if intVal != 1 && intVal != 2 {
return TimeRange{}, fmt.Errorf("invalid interval (%s)", s)
return TimeRange{}, fmt.Errorf("invalid half year interval (%s)", s)
}
switch intVal {
case 1:
return TimeRange{
Min: time.Date(yInt, time.Month(1), 1, 0, 0, 0, 0, time.UTC),
Max: time.Date(yInt, time.Month(7), 1, 0, 0, 0, 0, time.UTC).Add(-1),
MinSet: true,
MaxSet: true}, nil
return timeRangeBuilder(yInt, 1, yInt, 7), nil
case 2:
return TimeRange{
Min: time.Date(yInt, time.Month(7), 1, 0, 0, 0, 0, time.UTC),
Max: time.Date(yInt+1, time.Month(1), 1, 0, 0, 0, 0, time.UTC).Add(-1),
MinSet: true,
MaxSet: true}, nil
return timeRangeBuilder(yInt, 7, yInt+1, 1), nil
}
case "Q":
if intVal < 1 || intVal > 4 {
return TimeRange{}, fmt.Errorf("invalid interval (%s)", s)
return TimeRange{}, fmt.Errorf("invalid quarter interval (%s)", s)
}
switch intVal {
case 1:
return TimeRange{
Min: time.Date(yInt, time.Month(1), 1, 0, 0, 0, 0, time.UTC),
Max: time.Date(yInt, time.Month(4), 1, 0, 0, 0, 0, time.UTC).Add(-1),
MinSet: true,
MaxSet: true}, nil
return timeRangeBuilder(yInt, 1, yInt, 4), nil
case 2:
return TimeRange{
Min: time.Date(yInt, time.Month(4), 1, 0, 0, 0, 0, time.UTC),
Max: time.Date(yInt, time.Month(7), 1, 0, 0, 0, 0, time.UTC).Add(-1),
MinSet: true,
MaxSet: true}, nil
return timeRangeBuilder(yInt, 4, yInt, 7), nil
case 3:
return TimeRange{
Min: time.Date(yInt, time.Month(7), 1, 0, 0, 0, 0, time.UTC),
Max: time.Date(yInt, time.Month(10), 1, 0, 0, 0, 0, time.UTC).Add(-1),
MinSet: true,
MaxSet: true}, nil
return timeRangeBuilder(yInt, 7, yInt, 10), nil
case 4:
return TimeRange{
Min: time.Date(yInt, time.Month(10), 1, 0, 0, 0, 0, time.UTC),
Max: time.Date(yInt+1, time.Month(1), 1, 0, 0, 0, 0, time.UTC).Add(-1),
MinSet: true,
MaxSet: true}, nil
return timeRangeBuilder(yInt, 10, yInt+1, 1), nil
}
case "M":
if intVal < 1 || intVal > 12 {
return TimeRange{}, fmt.Errorf("invalid month interval (%s)", s)
}
switch intVal {
case 1:
return timeRangeBuilder(yInt, 1, yInt, 2), nil
case 2:
return timeRangeBuilder(yInt, 2, yInt, 3), nil
case 3:
return timeRangeBuilder(yInt, 3, yInt, 4), nil
case 4:
return timeRangeBuilder(yInt, 4, yInt, 5), nil
case 5:
return timeRangeBuilder(yInt, 5, yInt, 6), nil
case 6:
return timeRangeBuilder(yInt, 6, yInt, 7), nil
case 7:
return timeRangeBuilder(yInt, 7, yInt, 8), nil
case 8:
return timeRangeBuilder(yInt, 8, yInt, 9), nil
case 9:
return timeRangeBuilder(yInt, 9, yInt, 10), nil
case 10:
return timeRangeBuilder(yInt, 10, yInt, 11), nil
case 11:
return timeRangeBuilder(yInt, 11, yInt, 12), nil
case 12:
return timeRangeBuilder(yInt, 12, yInt+1, 1), nil
}
}
return TimeRange{}, fmt.Errorf("time range not supported (%s)", s)
}

func timeRangeBuilder(yMin, mMin, yMax, mMaxPlus1 int) TimeRange {
return TimeRange{
Min: time.Date(yMin, time.Month(mMin), 1, 0, 0, 0, 0, time.UTC),
Max: time.Date(yMax, time.Month(mMaxPlus1), 1, 0, 0, 0, 0, time.UTC).Add(-1),
MinSet: true,
MaxSet: true}
}

func (tr *TimeRange) Contains(t time.Time, inclusiveMin, inclusiveMax bool) (bool, error) {
if !tr.MinSet || !tr.MaxSet {
return false, errors.New("timerange must have min and max both set")
Expand Down
28 changes: 28 additions & 0 deletions time/timeutil/time_range_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,31 @@ func TestInRange(t *testing.T) {
}
}
}

var parseTimeRangeIntervalTests = []struct {
v string
tMin string
tMax string
}{
{"2023Q4", "2023-10-01T00:00:00Z", "2023-12-31T23:59:59.999999999Z"},
{"2023H2", "2023-07-01T00:00:00Z", "2023-12-31T23:59:59.999999999Z"},
{"2024H1", "2024-01-01T00:00:00Z", "2024-06-30T23:59:59.999999999Z"},
{"2024M1", "2024-01-01T00:00:00Z", "2024-01-31T23:59:59.999999999Z"},
}

func TestParseTimeRangeInterval(t *testing.T) {
for _, tt := range parseTimeRangeIntervalTests {
tr, err := ParseTimeRangeInterval(tt.v)
if err != nil {
t.Errorf("ParseTimeRangeInterval(\"%s\"): error (%s)", tt.v, err.Error())
}
tMin := MustParse(time.RFC3339Nano, tt.tMin).UTC()
if !tr.Min.Equal(tMin) {
t.Errorf("ParseTimeRangeInterval(\"%s\"): mismatch tr.Min want (%s) got (%s)", tt.v, tt.tMin, tMin.Format(time.RFC3339Nano))
}
tMax := MustParse(time.RFC3339Nano, tt.tMax).UTC()
if !tr.Max.Equal(tMax) {
t.Errorf("ParseTimeRangeInterval(\"%s\"): mismatch tr.Max want (%s) got (%s)", tt.v, tt.tMax, tMax.Format(time.RFC3339Nano))
}
}
}

0 comments on commit d3e4c30

Please sign in to comment.