From 908a6a9496fefb818f02664ea3059e1f3dc5d8ec Mon Sep 17 00:00:00 2001 From: Ragnis Armus Date: Sat, 20 May 2017 12:15:17 +0300 Subject: [PATCH 01/12] Implement reading of Float64Range --- ranges/float64range.go | 55 ++++++++++++++++++++++ ranges/parsing.go | 101 +++++++++++++++++++++++++++++++++++++++++ ranges/parsing_test.go | 39 ++++++++++++++++ 3 files changed, 195 insertions(+) create mode 100644 ranges/float64range.go create mode 100644 ranges/parsing.go create mode 100644 ranges/parsing_test.go diff --git a/ranges/float64range.go b/ranges/float64range.go new file mode 100644 index 000000000..0e52de5a0 --- /dev/null +++ b/ranges/float64range.go @@ -0,0 +1,55 @@ +package ranges + +import ( + "fmt" + "strconv" +) + +// Float64Range represents a range between two float64 values +type Float64Range struct { + Min float64 + MinInclusive bool + Max float64 + MaxInclusive bool +} + +// Scan implements the sql.Scanner interface +func (r *Float64Range) Scan(val interface{}) error { + if val == nil { + r.Min = 0 + r.MinInclusive = false + r.Max = 0 + r.MaxInclusive = false + return nil + } + minIncl, maxIncl, min, max, err := readFloatRange(val.([]byte)) + if err != nil { + return err + } + r.Min, err = strconv.ParseFloat(string(min), 64) + if err != nil { + return err + } + r.Max, err = strconv.ParseFloat(string(max), 64) + if err != nil { + return err + } + r.MinInclusive = minIncl + r.MaxInclusive = maxIncl + return nil +} + +// String returns a string representation of this range +func (r Float64Range) String() string { + var ( + open = "(" + close = ")" + ) + if r.MinInclusive { + open = "[" + } + if r.MaxInclusive { + close += "]" + } + return fmt.Sprintf("%s%f, %f%s", open, r.Min, r.Max, close) +} diff --git a/ranges/parsing.go b/ranges/parsing.go new file mode 100644 index 000000000..b2d8cfd8e --- /dev/null +++ b/ranges/parsing.go @@ -0,0 +1,101 @@ +package ranges + +import ( + "fmt" +) + +func readDigits(buf []byte, pos int) ([]byte, int, error) { + var s []byte + for pos < len(buf) && buf[pos] >= 48 && buf[pos] <= 57 { + s = append(s, buf[pos]) + pos++ + } + if len(s) == 0 { + return s, pos, fmt.Errorf("unexpected end of input at position %d", pos) + } + return s, pos, nil +} + +func readInteger(buf []byte, pos int) ([]byte, int, error) { + var s []byte + if pos < len(buf) && buf[pos] == '-' { + s = append(s, '-') + pos++ + } + digs, pos, err := readDigits(buf, pos) + if err != nil { + return nil, pos, err + } + return append(s, digs...), pos, nil +} + +func readFloat(buf []byte, pos int) ([]byte, int, error) { + s, pos, err := readInteger(buf, pos) + if err != nil { + return nil, pos, err + } + + if pos < len(buf) && buf[pos] == '.' { + var digs []byte + + s = append(s, '.') + pos++ + + digs, pos, err = readDigits(buf, pos) + if err != nil { + return nil, pos, err + } + s = append(s, digs...) + } + + return s, pos, nil +} + +func readSeparator(buf []byte, pos int) (int, error) { + if pos >= len(buf) { + return pos, fmt.Errorf("unexpected end of input at position %d", pos) + } + if buf[pos] != ',' { + return pos, fmt.Errorf("unexpected character '%c' at position %d", buf[pos], pos) + } + return pos + 1, nil +} + +func readRangeBound(buf []byte, pos int, incl, excl byte) (bool, int, error) { + if pos >= len(buf) { + return false, 0, fmt.Errorf("unexpected end of input at position %d", pos) + } + switch buf[pos] { + case incl: + return true, pos + 1, nil + case excl: + return false, pos + 1, nil + default: + return false, pos, fmt.Errorf("unexpected character '%c' at position %d", buf[pos], pos) + } +} + +func readFloatRange(buf []byte) (minIncl bool, maxIncl bool, min []byte, max []byte, err error) { + var pos int + minIncl, pos, err = readRangeBound(buf, pos, '[', '(') + if err != nil { + return + } + min, pos, err = readFloat(buf, pos) + if err != nil { + return + } + pos, err = readSeparator(buf, pos) + if err != nil { + return + } + max, pos, err = readFloat(buf, pos) + if err != nil { + return + } + maxIncl, pos, err = readRangeBound(buf, pos, ']', ')') + if err != nil { + return + } + return +} diff --git a/ranges/parsing_test.go b/ranges/parsing_test.go new file mode 100644 index 000000000..0ba3e3ea1 --- /dev/null +++ b/ranges/parsing_test.go @@ -0,0 +1,39 @@ +package ranges + +import ( + "testing" +) + +func TestReadFloatRange(t *testing.T) { + cases := []struct { + Input string + MinIn bool + MaxIn bool + Min string + Max string + }{ + {"[-1.23,98.0]", true, true, "-1.23", "98.0"}, + {"(1,2]", false, true, "1", "2"}, + {"[0,0.0]", true, true, "0", "0.0"}, + {"(1.29,-0.5)", false, false, "1.29", "-0.5"}, + } + + for _, tc := range cases { + minIn, maxIn, min, max, err := readFloatRange([]byte(tc.Input)) + if err != nil { + t.Fatalf("unexpected error: " + err.Error()) + } + if minIn != tc.MinIn { + t.Fatalf("expected min to be inclusive=%t, got %t", tc.MinIn, minIn) + } + if maxIn != tc.MaxIn { + t.Fatalf("expected max to be inclusive=%t, got %t", tc.MaxIn, maxIn) + } + if string(min) != tc.Min { + t.Fatalf("expected min to be '%s', got '%s'", tc.Min, min) + } + if string(max) != tc.Max { + t.Fatalf("expected max to be '%s', got '%s'", tc.Max, max) + } + } +} From f90a272019e3f1a4ff2c4cd773d5e2d19bece0ed Mon Sep 17 00:00:00 2001 From: Ragnis Armus Date: Sat, 20 May 2017 12:27:12 +0300 Subject: [PATCH 02/12] Make the range parsing more generic --- ranges/float64range.go | 6 ++-- ranges/parsing.go | 70 ++++++++++++++++-------------------------- ranges/parsing_test.go | 4 +-- 3 files changed, 31 insertions(+), 49 deletions(-) diff --git a/ranges/float64range.go b/ranges/float64range.go index 0e52de5a0..bbc5a1671 100644 --- a/ranges/float64range.go +++ b/ranges/float64range.go @@ -22,7 +22,7 @@ func (r *Float64Range) Scan(val interface{}) error { r.MaxInclusive = false return nil } - minIncl, maxIncl, min, max, err := readFloatRange(val.([]byte)) + minIn, maxIn, min, max, err := readRange(val.([]byte)) if err != nil { return err } @@ -34,8 +34,8 @@ func (r *Float64Range) Scan(val interface{}) error { if err != nil { return err } - r.MinInclusive = minIncl - r.MaxInclusive = maxIncl + r.MinInclusive = minIn + r.MaxInclusive = maxIn return nil } diff --git a/ranges/parsing.go b/ranges/parsing.go index b2d8cfd8e..49dacbc83 100644 --- a/ranges/parsing.go +++ b/ranges/parsing.go @@ -4,50 +4,32 @@ import ( "fmt" ) -func readDigits(buf []byte, pos int) ([]byte, int, error) { - var s []byte - for pos < len(buf) && buf[pos] >= 48 && buf[pos] <= 57 { - s = append(s, buf[pos]) - pos++ - } - if len(s) == 0 { - return s, pos, fmt.Errorf("unexpected end of input at position %d", pos) - } - return s, pos, nil -} - -func readInteger(buf []byte, pos int) ([]byte, int, error) { - var s []byte - if pos < len(buf) && buf[pos] == '-' { - s = append(s, '-') +func readNumber(buf []byte, pos int) ([]byte, int, error) { + var ( + s []byte + b byte + canEnd = false + inMantissa bool + ) + for pos < len(buf) { + b = buf[pos] + if b == '-' && len(s) == 0 { + s = append(s, b) + canEnd = false + } else if b >= 48 && b <= 57 { + s = append(s, b) + canEnd = true + } else if b == '.' && !inMantissa { + s = append(s, b) + canEnd = false + } else { + break + } pos++ } - digs, pos, err := readDigits(buf, pos) - if err != nil { - return nil, pos, err + if !canEnd { + return s, pos, fmt.Errorf("unexpected character '%c' at position %d", b, pos) } - return append(s, digs...), pos, nil -} - -func readFloat(buf []byte, pos int) ([]byte, int, error) { - s, pos, err := readInteger(buf, pos) - if err != nil { - return nil, pos, err - } - - if pos < len(buf) && buf[pos] == '.' { - var digs []byte - - s = append(s, '.') - pos++ - - digs, pos, err = readDigits(buf, pos) - if err != nil { - return nil, pos, err - } - s = append(s, digs...) - } - return s, pos, nil } @@ -75,13 +57,13 @@ func readRangeBound(buf []byte, pos int, incl, excl byte) (bool, int, error) { } } -func readFloatRange(buf []byte) (minIncl bool, maxIncl bool, min []byte, max []byte, err error) { +func readRange(buf []byte) (minIncl bool, maxIncl bool, min []byte, max []byte, err error) { var pos int minIncl, pos, err = readRangeBound(buf, pos, '[', '(') if err != nil { return } - min, pos, err = readFloat(buf, pos) + min, pos, err = readNumber(buf, pos) if err != nil { return } @@ -89,7 +71,7 @@ func readFloatRange(buf []byte) (minIncl bool, maxIncl bool, min []byte, max []b if err != nil { return } - max, pos, err = readFloat(buf, pos) + max, pos, err = readNumber(buf, pos) if err != nil { return } diff --git a/ranges/parsing_test.go b/ranges/parsing_test.go index 0ba3e3ea1..de7f76f36 100644 --- a/ranges/parsing_test.go +++ b/ranges/parsing_test.go @@ -4,7 +4,7 @@ import ( "testing" ) -func TestReadFloatRange(t *testing.T) { +func TestReadRange(t *testing.T) { cases := []struct { Input string MinIn bool @@ -19,7 +19,7 @@ func TestReadFloatRange(t *testing.T) { } for _, tc := range cases { - minIn, maxIn, min, max, err := readFloatRange([]byte(tc.Input)) + minIn, maxIn, min, max, err := readRange([]byte(tc.Input)) if err != nil { t.Fatalf("unexpected error: " + err.Error()) } From cffdfb0863a7303da87175507022a825471e99fb Mon Sep 17 00:00:00 2001 From: Ragnis Armus Date: Sat, 20 May 2017 12:43:38 +0300 Subject: [PATCH 03/12] Implement scanning of Int64Range --- ranges/int64range.go | 42 ++++++++++++++++++++++++++++++++++++++++++ ranges/parsing.go | 31 ++++++++++++++++++++++++++++--- 2 files changed, 70 insertions(+), 3 deletions(-) create mode 100644 ranges/int64range.go diff --git a/ranges/int64range.go b/ranges/int64range.go new file mode 100644 index 000000000..9547ba5a8 --- /dev/null +++ b/ranges/int64range.go @@ -0,0 +1,42 @@ +package ranges + +import ( + "errors" + "fmt" + "strconv" +) + +// Int64Range represents a range between two int64 values +type Int64Range struct { + Min int64 + Max int64 +} + +// Scan implements the sql.Scanner interface +func (r *Int64Range) Scan(val interface{}) error { + if val == nil { + return errors.New("cannot scan NULL into *Int64Range") + } + var ( + err error + min, max []byte + ) + min, max, err = readDiscreteRange(val.([]byte)) + if err != nil { + return err + } + r.Min, err = strconv.ParseInt(string(min), 10, 64) + if err != nil { + return err + } + r.Max, err = strconv.ParseInt(string(max), 10, 64) + if err != nil { + return err + } + return nil +} + +// String returns a string representation of this range +func (r Int64Range) String() string { + return fmt.Sprintf("[%d, %d)", r.Min, r.Max) +} diff --git a/ranges/parsing.go b/ranges/parsing.go index 49dacbc83..d0fce5c5d 100644 --- a/ranges/parsing.go +++ b/ranges/parsing.go @@ -33,11 +33,11 @@ func readNumber(buf []byte, pos int) ([]byte, int, error) { return s, pos, nil } -func readSeparator(buf []byte, pos int) (int, error) { +func readByte(buf []byte, pos int, expect byte) (int, error) { if pos >= len(buf) { return pos, fmt.Errorf("unexpected end of input at position %d", pos) } - if buf[pos] != ',' { + if buf[pos] != expect { return pos, fmt.Errorf("unexpected character '%c' at position %d", buf[pos], pos) } return pos + 1, nil @@ -67,7 +67,7 @@ func readRange(buf []byte) (minIncl bool, maxIncl bool, min []byte, max []byte, if err != nil { return } - pos, err = readSeparator(buf, pos) + pos, err = readByte(buf, pos, ',') if err != nil { return } @@ -81,3 +81,28 @@ func readRange(buf []byte) (minIncl bool, maxIncl bool, min []byte, max []byte, } return } + +func readDiscreteRange(buf []byte) (min []byte, max []byte, err error) { + var pos int + pos, err = readByte(buf, pos, '[') + if err != nil { + return + } + min, pos, err = readNumber(buf, pos) + if err != nil { + return + } + pos, err = readByte(buf, pos, ',') + if err != nil { + return + } + max, pos, err = readNumber(buf, pos) + if err != nil { + return + } + pos, err = readByte(buf, pos, ')') + if err != nil { + return + } + return +} From fb86b53f911ec9c13fb2a0c54b163a5c544727ea Mon Sep 17 00:00:00 2001 From: Ragnis Armus Date: Sat, 20 May 2017 12:49:48 +0300 Subject: [PATCH 04/12] Implement Int32Range type --- ranges/int32range.go | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 ranges/int32range.go diff --git a/ranges/int32range.go b/ranges/int32range.go new file mode 100644 index 000000000..7bff142aa --- /dev/null +++ b/ranges/int32range.go @@ -0,0 +1,41 @@ +package ranges + +import ( + "errors" + "fmt" + "strconv" +) + +// Int32Range represents a range between two int32 values. The minimum value is +// inclusive and the maximum is exclusive. +type Int32Range struct { + Min int32 + Max int32 +} + +// Scan implements the sql.Scanner interface +func (r *Int32Range) Scan(val interface{}) error { + if val == nil { + return errors.New("cannot scan NULL into *Int32Range") + } + minb, maxb, err := readDiscreteRange(val.([]byte)) + if err != nil { + return err + } + min, err := strconv.Atoi(string(minb)) + if err != nil { + return err + } + max, err := strconv.Atoi(string(maxb)) + if err != nil { + return err + } + r.Min = int32(min) + r.Max = int32(max) + return nil +} + +// String returns a string representation of this range +func (r Int32Range) String() string { + return fmt.Sprintf("[%d, %d)", r.Min, r.Max) +} From d21c52b28f3548d66279099a9ac3f232d0cb7932 Mon Sep 17 00:00:00 2001 From: Ragnis Armus Date: Sat, 20 May 2017 12:50:19 +0300 Subject: [PATCH 05/12] Add a comment about Int64Range inclusive/exclusive --- ranges/int64range.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ranges/int64range.go b/ranges/int64range.go index 9547ba5a8..31b172036 100644 --- a/ranges/int64range.go +++ b/ranges/int64range.go @@ -6,7 +6,8 @@ import ( "strconv" ) -// Int64Range represents a range between two int64 values +// Int64Range represents a range between two int64 values. The minimum value is +// inclusive and the maximum is exclusive. type Int64Range struct { Min int64 Max int64 From 3dda469f7fa775285dade834aeae51e294d339b5 Mon Sep 17 00:00:00 2001 From: Ragnis Armus Date: Sat, 20 May 2017 12:51:19 +0300 Subject: [PATCH 06/12] Make ranges' string representation match that of Postgres --- ranges/float64range.go | 2 +- ranges/int32range.go | 2 +- ranges/int64range.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ranges/float64range.go b/ranges/float64range.go index bbc5a1671..63bf42003 100644 --- a/ranges/float64range.go +++ b/ranges/float64range.go @@ -51,5 +51,5 @@ func (r Float64Range) String() string { if r.MaxInclusive { close += "]" } - return fmt.Sprintf("%s%f, %f%s", open, r.Min, r.Max, close) + return fmt.Sprintf("%s%f,%f%s", open, r.Min, r.Max, close) } diff --git a/ranges/int32range.go b/ranges/int32range.go index 7bff142aa..33f58626b 100644 --- a/ranges/int32range.go +++ b/ranges/int32range.go @@ -37,5 +37,5 @@ func (r *Int32Range) Scan(val interface{}) error { // String returns a string representation of this range func (r Int32Range) String() string { - return fmt.Sprintf("[%d, %d)", r.Min, r.Max) + return fmt.Sprintf("[%d,%d)", r.Min, r.Max) } diff --git a/ranges/int64range.go b/ranges/int64range.go index 31b172036..58cd03de2 100644 --- a/ranges/int64range.go +++ b/ranges/int64range.go @@ -39,5 +39,5 @@ func (r *Int64Range) Scan(val interface{}) error { // String returns a string representation of this range func (r Int64Range) String() string { - return fmt.Sprintf("[%d, %d)", r.Min, r.Max) + return fmt.Sprintf("[%d,%d)", r.Min, r.Max) } From ed68783d4e4d51cc7309ebc6a0faec096f7c607a Mon Sep 17 00:00:00 2001 From: Ragnis Armus Date: Sat, 20 May 2017 12:56:03 +0300 Subject: [PATCH 07/12] Fix a typo --- ranges/float64range.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ranges/float64range.go b/ranges/float64range.go index 63bf42003..7f14e82de 100644 --- a/ranges/float64range.go +++ b/ranges/float64range.go @@ -49,7 +49,7 @@ func (r Float64Range) String() string { open = "[" } if r.MaxInclusive { - close += "]" + close = "]" } return fmt.Sprintf("%s%f,%f%s", open, r.Min, r.Max, close) } From 07d14f3ae5527a94f9b4b458a2375e1e5ef6ef5a Mon Sep 17 00:00:00 2001 From: Ragnis Armus Date: Sat, 20 May 2017 12:59:36 +0300 Subject: [PATCH 08/12] Implement tests for range String functions --- ranges/float64range_test.go | 18 ++++++++++++++++++ ranges/int32range_test.go | 19 +++++++++++++++++++ ranges/int64range_test.go | 19 +++++++++++++++++++ 3 files changed, 56 insertions(+) create mode 100644 ranges/float64range_test.go create mode 100644 ranges/int32range_test.go create mode 100644 ranges/int64range_test.go diff --git a/ranges/float64range_test.go b/ranges/float64range_test.go new file mode 100644 index 000000000..6a4620c7e --- /dev/null +++ b/ranges/float64range_test.go @@ -0,0 +1,18 @@ +package ranges + +import ( + "testing" +) + +func TestFloat64RangeString(t *testing.T) { + test := func(min, max float64, minIn, maxIn bool, expect string) { + s := Float64Range{min, minIn, max, maxIn}.String() + if s != expect { + t.Errorf("expected '%s', got '%s'", expect, s) + } + } + + test(-1.0, 2.1, false, true, "(-1.000000,2.100000]") + test(9.99, 0.01, true, true, "[9.990000,0.010000]") + test(80.0, 90.0, false, false, "(80.000000,90.000000)") +} diff --git a/ranges/int32range_test.go b/ranges/int32range_test.go new file mode 100644 index 000000000..b2de514f0 --- /dev/null +++ b/ranges/int32range_test.go @@ -0,0 +1,19 @@ +package ranges + +import ( + "testing" +) + +func TestInt32RangeString(t *testing.T) { + test := func(min, max int32, expect string) { + s := Int32Range{min, max}.String() + if s != expect { + t.Errorf("expected '%s', got '%s'", expect, s) + } + } + + test(0, 2, "[0,2)") + test(0, 0, "[0,0)") + test(-2, 8, "[-2,8)") + test(8, -2, "[8,-2)") +} diff --git a/ranges/int64range_test.go b/ranges/int64range_test.go new file mode 100644 index 000000000..d04b8b73e --- /dev/null +++ b/ranges/int64range_test.go @@ -0,0 +1,19 @@ +package ranges + +import ( + "testing" +) + +func TestInt64RangeString(t *testing.T) { + test := func(min, max int64, expect string) { + s := Int64Range{min, max}.String() + if s != expect { + t.Errorf("expected '%s', got '%s'", expect, s) + } + } + + test(0, 2, "[0,2)") + test(0, 0, "[0,0)") + test(-2, 8, "[-2,8)") + test(8, -2, "[8,-2)") +} From 1f5a36a90679e47351ef159472d0c706fedbf9e9 Mon Sep 17 00:00:00 2001 From: Ragnis Armus Date: Sat, 20 May 2017 13:03:57 +0300 Subject: [PATCH 09/12] Implement Valuer interface for range types --- ranges/float64range.go | 6 ++++++ ranges/int32range.go | 6 ++++++ ranges/int64range.go | 6 ++++++ 3 files changed, 18 insertions(+) diff --git a/ranges/float64range.go b/ranges/float64range.go index 7f14e82de..540875a63 100644 --- a/ranges/float64range.go +++ b/ranges/float64range.go @@ -1,6 +1,7 @@ package ranges import ( + "database/sql/driver" "fmt" "strconv" ) @@ -39,6 +40,11 @@ func (r *Float64Range) Scan(val interface{}) error { return nil } +// Value implements the driver.Valuer interface +func (r Float64Range) Value() (driver.Value, error) { + return []byte(r.String()), nil +} + // String returns a string representation of this range func (r Float64Range) String() string { var ( diff --git a/ranges/int32range.go b/ranges/int32range.go index 33f58626b..c8233990a 100644 --- a/ranges/int32range.go +++ b/ranges/int32range.go @@ -1,6 +1,7 @@ package ranges import ( + "database/sql/driver" "errors" "fmt" "strconv" @@ -35,6 +36,11 @@ func (r *Int32Range) Scan(val interface{}) error { return nil } +// Value implements the driver.Valuer interface +func (r Int32Range) Value() (driver.Value, error) { + return []byte(r.String()), nil +} + // String returns a string representation of this range func (r Int32Range) String() string { return fmt.Sprintf("[%d,%d)", r.Min, r.Max) diff --git a/ranges/int64range.go b/ranges/int64range.go index 58cd03de2..265a2106f 100644 --- a/ranges/int64range.go +++ b/ranges/int64range.go @@ -1,6 +1,7 @@ package ranges import ( + "database/sql/driver" "errors" "fmt" "strconv" @@ -37,6 +38,11 @@ func (r *Int64Range) Scan(val interface{}) error { return nil } +// Value implements the driver.Valuer interface +func (r Int64Range) Value() (driver.Value, error) { + return []byte(r.String()), nil +} + // String returns a string representation of this range func (r Int64Range) String() string { return fmt.Sprintf("[%d,%d)", r.Min, r.Max) From 27416eaeb372843a7cd3c5e6ac59b91c7a030d9c Mon Sep 17 00:00:00 2001 From: Ragnis Armus Date: Sat, 20 May 2017 14:18:56 +0300 Subject: [PATCH 10/12] Implement DateRange type --- ranges/daterange.go | 104 +++++++++++++++++++++++++++++++++++++++ ranges/daterange_test.go | 63 ++++++++++++++++++++++++ ranges/parsing.go | 39 +++++++++++++++ 3 files changed, 206 insertions(+) create mode 100644 ranges/daterange.go create mode 100644 ranges/daterange_test.go diff --git a/ranges/daterange.go b/ranges/daterange.go new file mode 100644 index 000000000..382242f87 --- /dev/null +++ b/ranges/daterange.go @@ -0,0 +1,104 @@ +package ranges + +import ( + "database/sql/driver" + "errors" + "fmt" + "time" + + "github.com/lib/pq" +) + +func isTimeZero(t time.Time) bool { + return t.Hour() == 0 && t.Minute() == 0 && t.Second() == 0 && t.Nanosecond() == 0 +} + +// DateRange represents a range between two dates where the lower is inclusive +// and the upper exclusive. +type DateRange struct { + Lower time.Time + Upper time.Time +} + +// Scan implements the sql.Scanner interface +func (r *DateRange) Scan(val interface{}) error { + var ( + err error + minb, maxb []byte + ) + + if val == nil { + return errors.New("cannot scan NULL into *DateRange") + } + minb, maxb, err = readDiscreteTimeRange(val.([]byte)) + if err != nil { + return errors.New("could not scan date range: " + err.Error()) + } + + if len(minb) == 0 { + r.Lower = time.Time{} + } else { + r.Lower, err = pq.ParseTimestamp(nil, string(minb)) + if err != nil { + return errors.New("could not parse lower date:" + err.Error()) + } + if !isTimeZero(r.Lower) { + return errors.New("time component of lower date is not zero") + } + } + + if len(maxb) == 0 { + r.Upper = time.Time{} + } else { + r.Upper, err = pq.ParseTimestamp(nil, string(maxb)) + if err != nil { + return errors.New("could not parse upper date:" + err.Error()) + } + if !isTimeZero(r.Upper) { + return errors.New("time component of upper date is not zero") + } + } + + return nil +} + +// IsLowerInfinity returns whether the lower value is negative infinity +func (r DateRange) IsLowerInfinity() bool { + return r.Lower.IsZero() +} + +// IsUpperInfinity returns whether the upper value is positive infinity +func (r DateRange) IsUpperInfinity() bool { + return r.Upper.IsZero() +} + +// Value implements the driver.Value interface +func (r DateRange) Value() (driver.Value, error) { + if !isTimeZero(r.Lower) { + return nil, errors.New("time component of lower date is not zero") + } + if !isTimeZero(r.Upper) { + return nil, errors.New("time component of upper date is not zero") + } + if r.Lower.After(r.Upper) { + return nil, errors.New("lower date is after upper date") + } + return []byte(r.String()), nil +} + +// Returns the date range as a string where the dates are formatted according +// to ISO8601 +func (r DateRange) String() string { + var ( + open = '(' + lower, upper string + ) + if !r.Lower.IsZero() { + lower = r.Lower.Format("2006-01-02") + open = '[' + } + if !r.Upper.IsZero() { + upper = r.Upper.Format("2006-01-02") + } + return fmt.Sprintf("%c%s,%s)", open, lower, upper) +} diff --git a/ranges/daterange_test.go b/ranges/daterange_test.go new file mode 100644 index 000000000..c16657892 --- /dev/null +++ b/ranges/daterange_test.go @@ -0,0 +1,63 @@ +package ranges + +import ( + "testing" + "time" +) + +func TestDateRangeScan(t *testing.T) { + test := func(input string, lowers, uppers string) { + r := DateRange{} + if err := r.Scan([]byte(input)); err != nil { + t.Fatalf("unexpected error: " + err.Error()) + } + lower, _ := time.Parse("2006-01-02", lowers) + upper, _ := time.Parse("2006-01-02", uppers) + if !r.Lower.Equal(lower) { + t.Errorf("expected lower date '%v', got '%v'", lower, r.Lower) + } + if !r.Upper.Equal(upper) { + t.Errorf("expected upper date '%v', got '%v'", upper, r.Upper) + } + } + + test("[2000-01-01,2017-05-09)", "2000-01-01", "2017-05-09") + test("[2000-01-01,)", "2000-01-01", "0001-01-01") + test("[,2000-01-01)", "0001-01-01", "2000-01-01") +} + +func TestDateRangeString(t *testing.T) { + test := func(lowers, uppers string, expect string) { + var lower, upper time.Time + if lowers != "" { + lower, _ = time.Parse("2006-01-02", lowers) + } + if uppers != "" { + upper, _ = time.Parse("2006-01-02", uppers) + } + if s := (DateRange{lower, upper}).String(); s != expect { + t.Errorf("expected '%s', got '%s'", expect, s) + } + } + + test("2001-06-02", "2007-05-04", "[2001-06-02,2007-05-04)") + test("2001-06-02", "", "[2001-06-02,)") + test("", "2001-06-02", "(,2001-06-02)") + test("", "", "(,)") +} + +func TestDateRangeValueError(t *testing.T) { + expectError := func(lowers, uppers string) { + lower, _ := time.Parse("2006-01-02 15:04:05", lowers) + upper, _ := time.Parse("2006-01-02 15:04:05", uppers) + r := DateRange{lower, upper} + if _, err := r.Value(); err == nil { + t.Errorf("expected an error for '%s' but did not get one", r.String()) + } + } + + expectError("2001-01-02 00:00:00", "2001-01-01 00:00:00") + expectError("2001-02-01 00:00:00", "2001-01-01 00:00:00") + expectError("2001-02-01 12:00:03", "2001-01-01 00:00:00") + expectError("2001-02-01 00:00:00", "2001-01-01 13:00:00") +} diff --git a/ranges/parsing.go b/ranges/parsing.go index d0fce5c5d..b42f86b7a 100644 --- a/ranges/parsing.go +++ b/ranges/parsing.go @@ -1,6 +1,7 @@ package ranges import ( + "errors" "fmt" ) @@ -82,6 +83,15 @@ func readRange(buf []byte) (minIncl bool, maxIncl bool, min []byte, max []byte, return } +func readUntilTerminator(buf []byte, pos int, term byte) ([]byte, int, error) { + var s []byte + for pos < len(buf) && buf[pos] != term { + s = append(s, buf[pos]) + pos++ + } + return s, pos, nil +} + func readDiscreteRange(buf []byte) (min []byte, max []byte, err error) { var pos int pos, err = readByte(buf, pos, '[') @@ -106,3 +116,32 @@ func readDiscreteRange(buf []byte) (min []byte, max []byte, err error) { } return } + +func readDiscreteTimeRange(buf []byte) (min []byte, max []byte, err error) { + var pos int + minIn, pos, err := readRangeBound(buf, pos, '[', '(') + if err != nil { + return + } + min, pos, err = readUntilTerminator(buf, pos, ',') + if err != nil { + return + } + if !minIn && len(min) != 0 { + err = errors.New("lower value is marked as exclusive but does not have an empty value") + return + } + pos, err = readByte(buf, pos, ',') + if err != nil { + return + } + max, pos, err = readUntilTerminator(buf, pos, ')') + if err != nil { + return + } + pos, err = readByte(buf, pos, ')') + if err != nil { + return + } + return +} From a7f4b612306909d510566adbd3d3d731b9b5113c Mon Sep 17 00:00:00 2001 From: Ragnis Armus Date: Sat, 20 May 2017 14:33:29 +0300 Subject: [PATCH 11/12] Rename Min/Max to Lower/Upper for consistency with Postgres --- ranges/float64range.go | 32 ++++++++++++++++---------------- ranges/float64range_test.go | 4 ++-- ranges/int32range.go | 20 ++++++++++---------- ranges/int32range_test.go | 4 ++-- ranges/int64range.go | 20 ++++++++++---------- ranges/int64range_test.go | 4 ++-- 6 files changed, 42 insertions(+), 42 deletions(-) diff --git a/ranges/float64range.go b/ranges/float64range.go index 540875a63..763e049cf 100644 --- a/ranges/float64range.go +++ b/ranges/float64range.go @@ -8,35 +8,35 @@ import ( // Float64Range represents a range between two float64 values type Float64Range struct { - Min float64 - MinInclusive bool - Max float64 - MaxInclusive bool + Lower float64 + LowerInclusive bool + Upper float64 + UpperInclusive bool } // Scan implements the sql.Scanner interface func (r *Float64Range) Scan(val interface{}) error { if val == nil { - r.Min = 0 - r.MinInclusive = false - r.Max = 0 - r.MaxInclusive = false + r.Lower = 0 + r.LowerInclusive = false + r.Upper = 0 + r.UpperInclusive = false return nil } - minIn, maxIn, min, max, err := readRange(val.([]byte)) + lowerIn, upperIn, lower, upper, err := readRange(val.([]byte)) if err != nil { return err } - r.Min, err = strconv.ParseFloat(string(min), 64) + r.Lower, err = strconv.ParseFloat(string(lower), 64) if err != nil { return err } - r.Max, err = strconv.ParseFloat(string(max), 64) + r.Upper, err = strconv.ParseFloat(string(upper), 64) if err != nil { return err } - r.MinInclusive = minIn - r.MaxInclusive = maxIn + r.LowerInclusive = lowerIn + r.UpperInclusive = upperIn return nil } @@ -51,11 +51,11 @@ func (r Float64Range) String() string { open = "(" close = ")" ) - if r.MinInclusive { + if r.LowerInclusive { open = "[" } - if r.MaxInclusive { + if r.UpperInclusive { close = "]" } - return fmt.Sprintf("%s%f,%f%s", open, r.Min, r.Max, close) + return fmt.Sprintf("%s%f,%f%s", open, r.Lower, r.Upper, close) } diff --git a/ranges/float64range_test.go b/ranges/float64range_test.go index 6a4620c7e..edbcf5593 100644 --- a/ranges/float64range_test.go +++ b/ranges/float64range_test.go @@ -5,8 +5,8 @@ import ( ) func TestFloat64RangeString(t *testing.T) { - test := func(min, max float64, minIn, maxIn bool, expect string) { - s := Float64Range{min, minIn, max, maxIn}.String() + test := func(lower, upper float64, lowerIn, upperIn bool, expect string) { + s := Float64Range{lower, lowerIn, upper, upperIn}.String() if s != expect { t.Errorf("expected '%s', got '%s'", expect, s) } diff --git a/ranges/int32range.go b/ranges/int32range.go index c8233990a..fd867409e 100644 --- a/ranges/int32range.go +++ b/ranges/int32range.go @@ -7,11 +7,11 @@ import ( "strconv" ) -// Int32Range represents a range between two int32 values. The minimum value is -// inclusive and the maximum is exclusive. +// Int32Range represents a range between two int32 values. The lower value is +// inclusive and the upper is exclusive. type Int32Range struct { - Min int32 - Max int32 + Lower int32 + Upper int32 } // Scan implements the sql.Scanner interface @@ -19,20 +19,20 @@ func (r *Int32Range) Scan(val interface{}) error { if val == nil { return errors.New("cannot scan NULL into *Int32Range") } - minb, maxb, err := readDiscreteRange(val.([]byte)) + lowerb, upperb, err := readDiscreteRange(val.([]byte)) if err != nil { return err } - min, err := strconv.Atoi(string(minb)) + lower, err := strconv.Atoi(string(lowerb)) if err != nil { return err } - max, err := strconv.Atoi(string(maxb)) + upper, err := strconv.Atoi(string(upperb)) if err != nil { return err } - r.Min = int32(min) - r.Max = int32(max) + r.Lower = int32(lower) + r.Upper = int32(upper) return nil } @@ -43,5 +43,5 @@ func (r Int32Range) Value() (driver.Value, error) { // String returns a string representation of this range func (r Int32Range) String() string { - return fmt.Sprintf("[%d,%d)", r.Min, r.Max) + return fmt.Sprintf("[%d,%d)", r.Lower, r.Upper) } diff --git a/ranges/int32range_test.go b/ranges/int32range_test.go index b2de514f0..4f4fc27b2 100644 --- a/ranges/int32range_test.go +++ b/ranges/int32range_test.go @@ -5,8 +5,8 @@ import ( ) func TestInt32RangeString(t *testing.T) { - test := func(min, max int32, expect string) { - s := Int32Range{min, max}.String() + test := func(lower, upper int32, expect string) { + s := Int32Range{lower, upper}.String() if s != expect { t.Errorf("expected '%s', got '%s'", expect, s) } diff --git a/ranges/int64range.go b/ranges/int64range.go index 265a2106f..a114c7484 100644 --- a/ranges/int64range.go +++ b/ranges/int64range.go @@ -7,11 +7,11 @@ import ( "strconv" ) -// Int64Range represents a range between two int64 values. The minimum value is -// inclusive and the maximum is exclusive. +// Int64Range represents a range between two int64 values. The lower value is +// inclusive and the upper is exclusive. type Int64Range struct { - Min int64 - Max int64 + Lower int64 + Upper int64 } // Scan implements the sql.Scanner interface @@ -20,18 +20,18 @@ func (r *Int64Range) Scan(val interface{}) error { return errors.New("cannot scan NULL into *Int64Range") } var ( - err error - min, max []byte + err error + lower, upper []byte ) - min, max, err = readDiscreteRange(val.([]byte)) + lower, upper, err = readDiscreteRange(val.([]byte)) if err != nil { return err } - r.Min, err = strconv.ParseInt(string(min), 10, 64) + r.Lower, err = strconv.ParseInt(string(lower), 10, 64) if err != nil { return err } - r.Max, err = strconv.ParseInt(string(max), 10, 64) + r.Upper, err = strconv.ParseInt(string(upper), 10, 64) if err != nil { return err } @@ -45,5 +45,5 @@ func (r Int64Range) Value() (driver.Value, error) { // String returns a string representation of this range func (r Int64Range) String() string { - return fmt.Sprintf("[%d,%d)", r.Min, r.Max) + return fmt.Sprintf("[%d,%d)", r.Lower, r.Upper) } diff --git a/ranges/int64range_test.go b/ranges/int64range_test.go index d04b8b73e..f445d9281 100644 --- a/ranges/int64range_test.go +++ b/ranges/int64range_test.go @@ -5,8 +5,8 @@ import ( ) func TestInt64RangeString(t *testing.T) { - test := func(min, max int64, expect string) { - s := Int64Range{min, max}.String() + test := func(lower, upper int64, expect string) { + s := Int64Range{lower, upper}.String() if s != expect { t.Errorf("expected '%s', got '%s'", expect, s) } From 1c8b3da76bd63f66520b911691f414d54de05a0f Mon Sep 17 00:00:00 2001 From: Ragnis Armus Date: Sat, 20 May 2017 14:44:14 +0300 Subject: [PATCH 12/12] Return errors for invalid int ranges --- ranges/int32range.go | 18 ++++++------------ ranges/int32range_test.go | 13 +++++++++++++ ranges/int64range.go | 20 ++++++-------------- ranges/int64range_test.go | 13 +++++++++++++ ranges/intrange.go | 25 +++++++++++++++++++++++++ 5 files changed, 63 insertions(+), 26 deletions(-) create mode 100644 ranges/intrange.go diff --git a/ranges/int32range.go b/ranges/int32range.go index fd867409e..a78e94229 100644 --- a/ranges/int32range.go +++ b/ranges/int32range.go @@ -4,7 +4,6 @@ import ( "database/sql/driver" "errors" "fmt" - "strconv" ) // Int32Range represents a range between two int32 values. The lower value is @@ -19,25 +18,20 @@ func (r *Int32Range) Scan(val interface{}) error { if val == nil { return errors.New("cannot scan NULL into *Int32Range") } - lowerb, upperb, err := readDiscreteRange(val.([]byte)) + l, u, err := parseIntRange(val.([]byte), 32) if err != nil { return err } - lower, err := strconv.Atoi(string(lowerb)) - if err != nil { - return err - } - upper, err := strconv.Atoi(string(upperb)) - if err != nil { - return err - } - r.Lower = int32(lower) - r.Upper = int32(upper) + r.Lower = int32(l) + r.Upper = int32(u) return nil } // Value implements the driver.Valuer interface func (r Int32Range) Value() (driver.Value, error) { + if r.Lower > r.Upper { + return nil, errors.New("lower value is greater than the upper value") + } return []byte(r.String()), nil } diff --git a/ranges/int32range_test.go b/ranges/int32range_test.go index 4f4fc27b2..635a4946b 100644 --- a/ranges/int32range_test.go +++ b/ranges/int32range_test.go @@ -17,3 +17,16 @@ func TestInt32RangeString(t *testing.T) { test(-2, 8, "[-2,8)") test(8, -2, "[8,-2)") } + +func TestInt32RangeValue(t *testing.T) { + expectError := func(lower, upper int32) { + r := Int32Range{lower, upper} + if _, err := r.Value(); err == nil { + t.Errorf("expected an error for '%s' but did not get one", r.String()) + } + } + + expectError(2, 0) + expectError(8, -4) + expectError(-8, -9) +} diff --git a/ranges/int64range.go b/ranges/int64range.go index a114c7484..996e3c777 100644 --- a/ranges/int64range.go +++ b/ranges/int64range.go @@ -4,7 +4,6 @@ import ( "database/sql/driver" "errors" "fmt" - "strconv" ) // Int64Range represents a range between two int64 values. The lower value is @@ -19,27 +18,20 @@ func (r *Int64Range) Scan(val interface{}) error { if val == nil { return errors.New("cannot scan NULL into *Int64Range") } - var ( - err error - lower, upper []byte - ) - lower, upper, err = readDiscreteRange(val.([]byte)) - if err != nil { - return err - } - r.Lower, err = strconv.ParseInt(string(lower), 10, 64) - if err != nil { - return err - } - r.Upper, err = strconv.ParseInt(string(upper), 10, 64) + l, u, err := parseIntRange(val.([]byte), 64) if err != nil { return err } + r.Lower = l + r.Upper = u return nil } // Value implements the driver.Valuer interface func (r Int64Range) Value() (driver.Value, error) { + if r.Lower > r.Upper { + return nil, errors.New("lower value is greater than the upper value") + } return []byte(r.String()), nil } diff --git a/ranges/int64range_test.go b/ranges/int64range_test.go index f445d9281..389ff07f6 100644 --- a/ranges/int64range_test.go +++ b/ranges/int64range_test.go @@ -17,3 +17,16 @@ func TestInt64RangeString(t *testing.T) { test(-2, 8, "[-2,8)") test(8, -2, "[8,-2)") } + +func TestInt64RangeValue(t *testing.T) { + expectError := func(lower, upper int64) { + r := Int64Range{lower, upper} + if _, err := r.Value(); err == nil { + t.Errorf("expected an error for '%s' but did not get one", r.String()) + } + } + + expectError(2, 0) + expectError(8, -4) + expectError(-8, -9) +} diff --git a/ranges/intrange.go b/ranges/intrange.go new file mode 100644 index 000000000..5a5f93f00 --- /dev/null +++ b/ranges/intrange.go @@ -0,0 +1,25 @@ +package ranges + +import ( + "errors" + "strconv" +) + +func parseIntRange(buf []byte, bitSize int) (int64, int64, error) { + lowerb, upperb, err := readDiscreteRange(buf) + if err != nil { + return 0, 0, err + } + lower, err := strconv.ParseInt(string(lowerb), 10, bitSize) + if err != nil { + return 0, 0, err + } + upper, err := strconv.ParseInt(string(upperb), 10, bitSize) + if err != nil { + return 0, 0, err + } + if lower > upper { + return 0, 0, errors.New("lower value is greater than the upper value") + } + return lower, upper, nil +}