1
0
Fork 0
mirror of https://github.com/gocsaf/csaf.git synced 2025-12-22 11:55:40 +01:00

Accept days, months and years in time ranges. (#483)

This commit is contained in:
Sascha L. Teichmann 2023-10-19 13:13:11 +02:00 committed by GitHub
parent 81edb6ccbe
commit 455010dc64
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 105 additions and 8 deletions

View file

@ -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.

View file

@ -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

View file

@ -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 {