aboutsummaryrefslogtreecommitdiff
path: root/rfc3339
diff options
context:
space:
mode:
authorPaul Duncan <pabs@pablotron.org>2022-03-15 00:17:07 -0400
committerPaul Duncan <pabs@pablotron.org>2022-03-15 00:17:07 -0400
commita00d4d0fd4c7e9b4e5ecf97e3bb978f680541dba (patch)
treefe3f149cb41b456ead3db929f1b18c54edb1777f /rfc3339
parent3c678605abb7003085652cc2008dd8cbd4e3eec2 (diff)
downloadcvez-a00d4d0fd4c7e9b4e5ecf97e3bb978f680541dba.tar.bz2
cvez-a00d4d0fd4c7e9b4e5ecf97e3bb978f680541dba.zip
move cisa/date.go to rfc3339/date.go
Diffstat (limited to 'rfc3339')
-rw-r--r--rfc3339/date.go164
-rw-r--r--rfc3339/date_test.go374
2 files changed, 538 insertions, 0 deletions
diff --git a/rfc3339/date.go b/rfc3339/date.go
new file mode 100644
index 0000000..4f11d03
--- /dev/null
+++ b/rfc3339/date.go
@@ -0,0 +1,164 @@
+package rfc3339
+
+import (
+ "encoding/json"
+ "fmt"
+ "regexp"
+ "strconv"
+)
+
+// RFC3339 date.
+type Date uint16
+
+// Returns true if the year is a leap year, and false otherwise.
+//
+// Reference:
+// https://www.timeanddate.com/date/leapyear.html
+func isLeapYear(y uint16) bool {
+ // leap year rules:
+ // 1. year is evenly divisible by 4 and is not evenly divisible by 100
+ // 2. year is evenly divisible by 4 and is evenly divisible by 400
+ return (((y % 4) == 0) && ((y % 100) != 0)) || // rule 1
+ (((y % 4) == 0) && ((y % 400) == 0)) // rule 2
+}
+
+// maximum month days
+var maxMonthDays = []uint16 {
+ 31, // jan
+ 28, // feb (incorrect for leap years!)
+ 31, // mar
+ 30, // apr
+ 31, // may
+ 30, // jun
+ 31, // jul
+ 31, // aug
+ 30, // sep
+ 31, // oct
+ 30, // nov
+ 31, // dec
+}
+
+// Get the maximum day for the given year and month.
+func getMaxMonthDay(y, m uint16) uint16 {
+ if m == 2 && isLeapYear(y) {
+ return 29
+ } else {
+ return maxMonthDays[m - 1]
+ }
+}
+
+// Parse date component and check range.
+func parseDateComponent(name string, s []byte, min, max uint16) (uint16, error) {
+ // parse value
+ vr, err := strconv.ParseUint(string(s), 10, 32)
+ if err != nil {
+ return 0, err
+ }
+
+ v := uint16(vr)
+
+ // check range
+ if v < min || v > max {
+ return 0, fmt.Errorf("%s component out of range [%d, %d]: %d", name, v, min, max)
+ }
+
+ return uint16(v), nil
+}
+
+var dateRe = regexp.MustCompile(`^\d{4}-\d{2}-\d{2}$`)
+
+// Create date from byte slice.
+func NewDate(s []byte) (Date, error) {
+ var r Date
+
+ if !dateRe.Match(s) {
+ return r, fmt.Errorf("invalid date: \"%s\"", s)
+ }
+
+ // parse year
+ y, err := parseDateComponent("year", s[0:4], 1999, 2126)
+ if err != nil {
+ return r, err
+ }
+
+ // parse month
+ m, err := parseDateComponent("month", s[5:7], 1, 12)
+ if err != nil {
+ return r, err
+ }
+
+ // parse day
+ d, err := parseDateComponent("month", s[8:10], 1, getMaxMonthDay(y, m))
+ if err != nil {
+ return r, err
+ }
+
+ // encode return date
+ r = Date((((y - 1999) & 0x3f) << 9) |
+ (((m - 1) & 0xf) << 5) |
+ ((d - 1) & 0x1f))
+
+ // return result
+ return r, nil
+}
+
+// Get year, month, and day components.
+func (me Date) GetComponents() (uint16, uint16, uint16) {
+ // extract components
+ y := uint16((uint16(me) >> 9) + 1999)
+ m := uint16(((uint16(me) >> 5) & 0xf) + 1)
+ d := uint16((uint16(me) & 0x1f) + 1)
+
+ return y, m, d
+}
+
+// Get year component.
+func (me Date) Year() uint16 {
+ y, _, _ := me.GetComponents()
+ return y
+}
+
+// Get month component.
+func (me Date) Month() uint16 {
+ _, m, _ := me.GetComponents()
+ return m
+}
+
+// Get day component.
+func (me Date) Day() uint16 {
+ _, _, d := me.GetComponents()
+ return d
+}
+
+// Convert to string.
+func (me Date) String() string {
+ // extract date components
+ y, m, d := me.GetComponents()
+
+ // return string
+ return fmt.Sprintf("%04d-%02d-%02d", y, m, d)
+}
+
+// Unmarshal date from JSON string.
+func (me *Date) UnmarshalJSON(b []byte) error {
+ // decode json string
+ var s string
+ if err := json.Unmarshal(b, &s); err != nil {
+ return err
+ }
+
+ // create date
+ d, err := NewDate([]byte(s))
+ if err != nil {
+ return err
+ }
+
+ // save result, return success
+ *me = d
+ return nil
+}
+
+// Marshal Date as JSON string.
+func (d Date) MarshalJSON() ([]byte, error) {
+ return json.Marshal(d.String())
+}
diff --git a/rfc3339/date_test.go b/rfc3339/date_test.go
new file mode 100644
index 0000000..ab70377
--- /dev/null
+++ b/rfc3339/date_test.go
@@ -0,0 +1,374 @@
+package rfc3339
+
+import (
+ "encoding/json"
+ "fmt"
+ "reflect"
+ "testing"
+)
+
+func TestIsLeapYear(t *testing.T) {
+ tests := []struct {
+ val uint16
+ exp bool
+ } {
+ { 1800, false },
+ { 1900, false },
+ { 1996, true },
+ { 1997, false },
+ { 1998, false },
+ { 1999, false },
+ { 2000, true },
+ { 2001, false },
+ { 2002, false },
+ { 2003, false },
+ { 2004, true },
+ { 2005, false },
+ { 2006, false },
+ { 2007, false },
+ { 2008, true },
+ { 2009, false },
+ { 2010, false },
+ { 2011, false },
+ { 2012, true },
+ { 2013, false },
+ { 2014, false },
+ { 2015, false },
+ { 2016, true },
+ { 2017, false },
+ { 2018, false },
+ { 2019, false },
+ { 2020, true },
+ { 2021, false },
+ { 2022, false },
+ { 2023, false },
+ { 2024, true },
+ { 2100, false },
+ { 2400, true },
+ }
+
+ for _, test := range(tests) {
+ t.Run(fmt.Sprintf("%d", test.val), func(t *testing.T) {
+ got := isLeapYear(test.val)
+ if got != test.exp {
+ t.Errorf("got %v, exp %v", got, test.exp)
+ }
+ })
+ }
+}
+
+func TestGetMaxMonthDay(t *testing.T) {
+ tests := []struct {
+ y uint16
+ m uint16
+ exp uint16
+ } {
+ { 2000, 2, 29 }, // test leap year
+ { 2001, 2, 28 }, // test non-leap year
+ { 2022, 1, 31 }, // test full non-leap year
+ { 2022, 2, 28 },
+ { 2022, 3, 31 },
+ { 2022, 4, 30 },
+ { 2022, 5, 31 },
+ { 2022, 6, 30 },
+ { 2022, 7, 31 },
+ { 2022, 8, 31 },
+ { 2022, 9, 30 },
+ { 2022, 10, 31 },
+ { 2022, 11, 30 },
+ { 2022, 12, 31 },
+ }
+
+ for _, test := range(tests) {
+ t.Run(fmt.Sprintf("%04d-%02d", test.y, test.m), func(t *testing.T) {
+ got := getMaxMonthDay(test.y, test.m)
+ if got != test.exp {
+ t.Errorf("got %v, exp %v", got, test.exp)
+ }
+ })
+ }
+}
+
+func TestParseDateComponent(t *testing.T) {
+ tests := []struct {
+ name string
+ val string
+ min uint16
+ max uint16
+ exp bool
+ } {
+ { "bad-uint", "abcd", 2000, 2001, false },
+ { "y-pass", "2000", 2000, 2001, true },
+ { "y-fail-lo", "1999", 2000, 2001, false },
+ { "y-fail-hi", "2002", 2000, 2001, false },
+ { "m-pass-1", "1", 1, 12, true },
+ { "m-pass-2", "2", 1, 12, true },
+ { "m-pass-3", "3", 1, 12, true },
+ { "m-pass-4", "4", 1, 12, true },
+ { "m-pass-5", "5", 1, 12, true },
+ { "m-pass-6", "6", 1, 12, true },
+ { "m-pass-7", "7", 1, 12, true },
+ { "m-pass-8", "8", 1, 12, true },
+ { "m-pass-9", "9", 1, 12, true },
+ { "m-pass-10", "10", 1, 12, true },
+ { "m-pass-11", "11", 1, 12, true },
+ { "m-pass-12", "12", 1, 12, true },
+ { "m-fail-lo", "0", 1, 12, false },
+ { "m-fail-hi", "13", 1, 12, false },
+ { "d-pass-1", "1", 1, 31, true },
+ { "d-pass-10", "10", 1, 31, true },
+ { "d-pass-30", "10", 1, 31, true },
+ { "d-fail-lo", "0", 1, 31, false },
+ { "d-fail-hi", "32", 1, 31, false },
+ }
+
+ for _, test := range(tests) {
+ t.Run(test.name, func(t *testing.T) {
+ got, err := parseDateComponent("a", []byte(test.val), test.min, test.max)
+ if err != nil && test.exp == true {
+ t.Error(err)
+ } else if err == nil && test.exp == false {
+ t.Errorf("got %v, exp error", got)
+ }
+ })
+ }
+}
+
+func TestNewDate(t *testing.T) {
+ tests := []struct {
+ name string
+ val string
+ exp bool
+ } {
+ { "pass", "2000-01-03", true },
+ { "pass-lo", "1999-01-01", true },
+ { "pass-hi", "2126-12-31", true },
+ { "fail", "asdf", false },
+ { "fail-y-lo", "1998-01-03", false },
+ { "fail-y-hi", "2128-01-03", false },
+ { "fail-m-lo", "2126-00-03", false },
+ { "fail-m-hi", "2126-13-03", false },
+ { "fail-d-lo", "2126-01-00", false },
+ { "fail-d-hi", "2126-01-32", false },
+ { "fail-d-hi", "2126-02-29", false },
+ { "fail-d-hi", "2126-03-32", false },
+ { "fail-d-hi", "2126-04-31", false },
+ { "fail-d-hi", "2126-05-32", false },
+ { "fail-d-hi", "2126-06-31", false },
+ }
+
+ for _, test := range(tests) {
+ t.Run(test.name, func(t *testing.T) {
+ if got, err := NewDate([]byte(test.val)); err != nil && test.exp {
+ t.Error(err)
+ } else if err == nil && !test.exp {
+ t.Errorf("got %v, exp error", got)
+ }
+ })
+ }
+}
+
+func TestGetComponents(t *testing.T) {
+ type date struct {
+ y uint16
+ m uint16
+ d uint16
+ }
+
+ tests := []struct {
+ val string
+ exp date
+ } {
+ { "2022-10-31", date { 2022, 10, 31 } },
+ }
+
+ for _, test := range(tests) {
+ t.Run(test.val, func(t *testing.T) {
+ // create date
+ dt, err := NewDate([]byte(test.val))
+ if err != nil {
+ t.Error(err)
+ return
+ }
+
+ // get components
+ y, m, d := dt.GetComponents()
+ got := date { y, m, d }
+
+ // check components
+ if !reflect.DeepEqual(got, test.exp) {
+ t.Errorf("got %v, exp %v", got, test.exp)
+ }
+ })
+ }
+}
+
+func TestYear(t *testing.T) {
+ tests := []struct {
+ val string
+ exp uint16
+ } {
+ { "2022-10-31", 2022 },
+ }
+
+ for _, test := range(tests) {
+ t.Run(test.val, func(t *testing.T) {
+ // create date
+ dt, err := NewDate([]byte(test.val))
+ if err != nil {
+ t.Error(err)
+ return
+ }
+
+ // get year
+ got := dt.Year()
+
+ // check components
+ if got != test.exp {
+ t.Errorf("got year %d, exp %d", got, test.exp)
+ }
+ })
+ }
+}
+
+func TestMonth(t *testing.T) {
+ tests := []struct {
+ val string
+ exp uint16
+ } {
+ { "2022-10-31", 10 },
+ }
+
+ for _, test := range(tests) {
+ t.Run(test.val, func(t *testing.T) {
+ // create date
+ dt, err := NewDate([]byte(test.val))
+ if err != nil {
+ t.Error(err)
+ return
+ }
+
+ // get month
+ got := dt.Month()
+
+ // check components
+ if got != test.exp {
+ t.Errorf("got month %d, exp %d", got, test.exp)
+ }
+ })
+ }
+}
+
+func TestDay(t *testing.T) {
+ tests := []struct {
+ val string
+ exp uint16
+ } {
+ { "2022-10-31", 31 },
+ }
+
+ for _, test := range(tests) {
+ t.Run(test.val, func(t *testing.T) {
+ // create date
+ dt, err := NewDate([]byte(test.val))
+ if err != nil {
+ t.Error(err)
+ return
+ }
+
+ // get day
+ got := dt.Day()
+
+ // check components
+ if got != test.exp {
+ t.Errorf("got day %d, exp %d", got, test.exp)
+ }
+ })
+ }
+}
+
+func TestString(t *testing.T) {
+ tests := []string {
+ "2022-10-31",
+ }
+
+ for _, test := range(tests) {
+ t.Run(test, func(t *testing.T) {
+ // create date
+ dt, err := NewDate([]byte(test))
+ if err != nil {
+ t.Error(err)
+ return
+ }
+
+ // get/check string
+ got := dt.String()
+ if got != test {
+ t.Errorf("got %s, exp %s", got, test)
+ }
+ })
+ }
+}
+
+func TestDateUnmarshalJSON(t *testing.T) {
+ passTests := []struct {
+ val string
+ exp string
+ } {
+ { `"2022-02-03"`, "2022-02-03" },
+ }
+
+ for _, test := range(passTests) {
+ var got Date
+ if err := json.Unmarshal([]byte(test.val), &got); err != nil {
+ t.Error(err)
+ } else if got.String() != test.exp {
+ t.Errorf("got \"%s\", exp \"%s\"", got.String(), test.exp)
+ }
+ }
+
+ failTests := []struct {
+ name string
+ val string
+ } {
+ { "fail-str", "asdf" },
+ { "fail-parse", "\"asdf\"" },
+ }
+
+ for _, test := range(failTests) {
+ t.Run(test.name, func(t *testing.T) {
+ var got Date
+ if err := got.UnmarshalJSON([]byte(test.val)); err == nil {
+ t.Errorf("got \"%v\" exp error", got)
+ }
+ })
+ }
+
+}
+
+func TestDateMarshalJSON(t *testing.T) {
+ tests := []struct {
+ val string
+ exp string
+ } {
+ { "2022-10-31", `"2022-10-31"` },
+ }
+
+ for _, test := range(tests) {
+ t.Run(test.val, func(t *testing.T) {
+ // create date
+ dt, err := NewDate([]byte(test.val))
+ if err != nil {
+ t.Error(err)
+ return
+ }
+
+ // get/check string
+ if got, err := dt.MarshalJSON(); err != nil {
+ t.Error(err)
+ } else if string(got) != test.exp {
+ t.Errorf("got \"%s\", exp \"%s\"", got, test.exp)
+ }
+ })
+ }
+}