diff options
-rw-r--r-- | cisa/date.go | 139 | ||||
-rw-r--r-- | cisa/date_test.go | 310 |
2 files changed, 449 insertions, 0 deletions
diff --git a/cisa/date.go b/cisa/date.go new file mode 100644 index 0000000..b36b73c --- /dev/null +++ b/cisa/date.go @@ -0,0 +1,139 @@ +package cisa + +import ( + "fmt" + "regexp" + "strconv" +) + +// YYYY-MM-DD +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("%d-%d-%d", y, m, d) +} diff --git a/cisa/date_test.go b/cisa/date_test.go new file mode 100644 index 0000000..c64f618 --- /dev/null +++ b/cisa/date_test.go @@ -0,0 +1,310 @@ +package cisa + +import ( + "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) + } + }) + } +} |