Skip to content

Commit

Permalink
Accept days, months and years in time ranges. (#483)
Browse files Browse the repository at this point in the history
  • Loading branch information
s-l-teichmann authored Oct 19, 2023
1 parent 81edb6c commit 455010d
Show file tree
Hide file tree
Showing 3 changed files with 105 additions and 8 deletions.
13 changes: 8 additions & 5 deletions docs/csaf_downloader.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,11 +102,14 @@ into a given intervall. There are three possible notations:
1. Relative. If the given string follows the rules of a
[Go duration](https://pkg.go.dev/[email protected]#ParseDuration),
the time interval from now going back that duration is used.
Some examples:
- `"3h"` means downloading the advisories that have changed in the last three hours.
- `"30m"` .. changed within the last thirty minutes.
- `"72h"` .. changed within the last three days.
- `"8760h"` .. changed within the last 365 days.
In extension to this the suffixes 'd' for days, 'M' for month
and 'y' for years are recognized. In these cases only integer
values are accepted without any fractions.
Some examples:
- `"3h"` means downloading the advisories that have changed in the last three hours.
- `"30m"` .. changed within the last thirty minutes.
- `"3M2m"` .. changed within the last three months and two minutes.
- `"2y"` .. changed within the last two years.

2. Absolute. If the given string is an RFC 3339 date timestamp
the time interval between this date and now is used.
Expand Down
69 changes: 66 additions & 3 deletions internal/models/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ package models
import (
"encoding/json"
"fmt"
"regexp"
"strconv"
"strings"
"sync"
"time"
)

Expand Down Expand Up @@ -59,6 +62,65 @@ func (tr *TimeRange) UnmarshalText(text []byte) error {
return tr.UnmarshalFlag(string(text))
}

var (
yearsMonthsDays *regexp.Regexp
yearsMonthsDaysOnce sync.Once
)

// parseDuration extends time.ParseDuration with recognition of
// years, month and days with the suffixes "y", "M" and "d".
// Onlys integer values are detected. The handling of fractional
// values would increase the complexity and may be done in the future.
// The calculate dates are assumed to be before the reference time.
func parseDuration(s string, reference time.Time) (time.Duration, error) {
var (
extra time.Duration
err error
used bool
)
parse := func(s string) int {
if err == nil {
var v int
v, err = strconv.Atoi(s)
return v
}
return 0
}
// Only compile expression if really needed.
yearsMonthsDaysOnce.Do(func() {
yearsMonthsDays = regexp.MustCompile(`[-+]?[0-9]+[yMd]`)
})
s = yearsMonthsDays.ReplaceAllStringFunc(s, func(part string) string {
used = true
var years, months, days int
switch suf, num := part[len(part)-1], part[:len(part)-1]; suf {
case 'y':
years = -parse(num)
case 'M':
months = -parse(num)
case 'd':
days = -parse(num)
}
date := reference.AddDate(years, months, days)
extra += reference.Sub(date)
// Remove from string
return ""
})
if err != nil {
return 0, err
}
// If there is no rest we don't need the stdlib parser.
if used && s == "" {
return extra, nil
}
// Parse the rest with the stdlib.
d, err := time.ParseDuration(s)
if err != nil {
return d, err
}
return d + extra, nil
}

// MarshalJSON implements [encoding/json.Marshaler].
func (tr TimeRange) MarshalJSON() ([]byte, error) {
s := []string{
Expand All @@ -72,9 +134,10 @@ func (tr TimeRange) MarshalJSON() ([]byte, error) {
func (tr *TimeRange) UnmarshalFlag(s string) error {
s = strings.TrimSpace(s)

now := time.Now()

// Handle relative case first.
if duration, err := time.ParseDuration(s); err == nil {
now := time.Now()
if duration, err := parseDuration(s, now); err == nil {
*tr = NewTimeInterval(now.Add(-duration), now)
return nil
}
Expand All @@ -88,7 +151,7 @@ func (tr *TimeRange) UnmarshalFlag(s string) error {
if !ok {
return fmt.Errorf("%q is not a valid RFC date time", a)
}
*tr = NewTimeInterval(start, time.Now())
*tr = NewTimeInterval(start, now)
return nil
}
// Real interval
Expand Down
31 changes: 31 additions & 0 deletions internal/models/models_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
package models

import (
"strings"
"testing"
"time"
)
Expand All @@ -25,6 +26,36 @@ func TestNewTimeInterval(t *testing.T) {
}
}

func TestParseDuration(t *testing.T) {

now := time.Now()

for _, x := range []struct {
in string
expected time.Duration
reference time.Time
fail bool
}{
{"1h", time.Hour, now, false},
{"2y", now.Sub(now.AddDate(-2, 0, 0)), now, false},
{"13M", now.Sub(now.AddDate(0, -13, 0)), now, false},
{"31d", now.Sub(now.AddDate(0, 0, -31)), now, false},
{"1h2d3m", now.Sub(now.AddDate(0, 0, -2)) + time.Hour + 3*time.Minute, now, false},
{strings.Repeat("1", 70) + "y1d", 0, now, true},
} {
got, err := parseDuration(x.in, x.reference)
if err != nil {
if !x.fail {
t.Errorf("%q should not fail: %v", x.in, err)
}
continue
}
if got != x.expected {
t.Errorf("%q got %v expected %v", x.in, got, x.expected)
}
}
}

// TestGuessDate tests whether a sample of strings are correctly parsed into Dates by guessDate()
func TestGuessDate(t *testing.T) {
if _, guess := guessDate("2006-01-02T15:04:05"); !guess {
Expand Down

0 comments on commit 455010d

Please sign in to comment.