mirror of
https://github.com/gocsaf/csaf.git
synced 2025-12-22 05:40:11 +01:00
Accept days, months and years in time ranges. (#483)
This commit is contained in:
parent
81edb6ccbe
commit
455010dc64
3 changed files with 105 additions and 8 deletions
|
|
@ -102,11 +102,14 @@ into a given intervall. There are three possible notations:
|
||||||
1. Relative. If the given string follows the rules of a
|
1. Relative. If the given string follows the rules of a
|
||||||
[Go duration](https://pkg.go.dev/time@go1.20.6#ParseDuration),
|
[Go duration](https://pkg.go.dev/time@go1.20.6#ParseDuration),
|
||||||
the time interval from now going back that duration is used.
|
the time interval from now going back that duration is used.
|
||||||
Some examples:
|
In extension to this the suffixes 'd' for days, 'M' for month
|
||||||
- `"3h"` means downloading the advisories that have changed in the last three hours.
|
and 'y' for years are recognized. In these cases only integer
|
||||||
- `"30m"` .. changed within the last thirty minutes.
|
values are accepted without any fractions.
|
||||||
- `"72h"` .. changed within the last three days.
|
Some examples:
|
||||||
- `"8760h"` .. changed within the last 365 days.
|
- `"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
|
2. Absolute. If the given string is an RFC 3339 date timestamp
|
||||||
the time interval between this date and now is used.
|
the time interval between this date and now is used.
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,10 @@ package models
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -59,6 +62,65 @@ func (tr *TimeRange) UnmarshalText(text []byte) error {
|
||||||
return tr.UnmarshalFlag(string(text))
|
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].
|
// MarshalJSON implements [encoding/json.Marshaler].
|
||||||
func (tr TimeRange) MarshalJSON() ([]byte, error) {
|
func (tr TimeRange) MarshalJSON() ([]byte, error) {
|
||||||
s := []string{
|
s := []string{
|
||||||
|
|
@ -72,9 +134,10 @@ func (tr TimeRange) MarshalJSON() ([]byte, error) {
|
||||||
func (tr *TimeRange) UnmarshalFlag(s string) error {
|
func (tr *TimeRange) UnmarshalFlag(s string) error {
|
||||||
s = strings.TrimSpace(s)
|
s = strings.TrimSpace(s)
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
// Handle relative case first.
|
// Handle relative case first.
|
||||||
if duration, err := time.ParseDuration(s); err == nil {
|
if duration, err := parseDuration(s, now); err == nil {
|
||||||
now := time.Now()
|
|
||||||
*tr = NewTimeInterval(now.Add(-duration), now)
|
*tr = NewTimeInterval(now.Add(-duration), now)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
@ -88,7 +151,7 @@ func (tr *TimeRange) UnmarshalFlag(s string) error {
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("%q is not a valid RFC date time", a)
|
return fmt.Errorf("%q is not a valid RFC date time", a)
|
||||||
}
|
}
|
||||||
*tr = NewTimeInterval(start, time.Now())
|
*tr = NewTimeInterval(start, now)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
// Real interval
|
// Real interval
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
package models
|
package models
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
@ -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()
|
// TestGuessDate tests whether a sample of strings are correctly parsed into Dates by guessDate()
|
||||||
func TestGuessDate(t *testing.T) {
|
func TestGuessDate(t *testing.T) {
|
||||||
if _, guess := guessDate("2006-01-02T15:04:05"); !guess {
|
if _, guess := guessDate("2006-01-02T15:04:05"); !guess {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue