From 3001e8a8ce3f4f0cbe17e64b0d4183bc47b0e722 Mon Sep 17 00:00:00 2001 From: vanshavenger Date: Mon, 23 Dec 2024 03:00:18 +0530 Subject: [PATCH 1/8] New constants --- internal/eval/constants.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/internal/eval/constants.go b/internal/eval/constants.go index 5c9b1ab3c..6cfb2731e 100644 --- a/internal/eval/constants.go +++ b/internal/eval/constants.go @@ -66,4 +66,7 @@ const ( FILTERS string = "FILTER" ITEMS string = "ITEMS" EXPANSION string = "EXPANSION" + BYSCORE string = "BYSCORE" + BYLEX string = "BYLEX" + LIMIT string = "LIMIT" ) From 249a5afb22d5ff21d0d24a5845e0ce74e02e6223 Mon Sep 17 00:00:00 2001 From: vanshavenger Date: Mon, 23 Dec 2024 03:08:58 +0530 Subject: [PATCH 2/8] Zrange Implementation --- internal/eval/sortedset/sorted_set.go | 49 ++++++ internal/eval/store_eval.go | 232 +++++++++++++++++++++++--- 2 files changed, 255 insertions(+), 26 deletions(-) diff --git a/internal/eval/sortedset/sorted_set.go b/internal/eval/sortedset/sorted_set.go index 54dc857aa..1429b5146 100644 --- a/internal/eval/sortedset/sorted_set.go +++ b/internal/eval/sortedset/sorted_set.go @@ -325,3 +325,52 @@ func DeserializeSortedSet(buf *bytes.Reader) (*Set, error) { return ss, nil } + +// GetRangeByScore returns a slice of members with scores between min and max, inclusive. +func (ss *Set) GetRangeByScore(min, max float64, withScores bool, reverse bool, offset, count int) []string { + var result []string + index := 0 + returned := 0 + + iterFunc := func(item btree.Item) bool { + ssi := item.(*Item) + + if reverse { + if ssi.Score > max { + return true + } + if ssi.Score < min { + return false + } + } else { + if ssi.Score < min { + return true + } + if ssi.Score > max { + return false + } + } + + if index >= offset { + result = append(result, ssi.Member) + if withScores { + scoreStr := strings.ToLower(strconv.FormatFloat(ssi.Score, 'g', -1, 64)) + result = append(result, scoreStr) + } + returned++ + if count > 0 && returned >= count { + return false + } + } + index++ + return true + } + + if reverse { + ss.tree.Descend(iterFunc) + } else { + ss.tree.Ascend(iterFunc) + } + + return result +} diff --git a/internal/eval/store_eval.go b/internal/eval/store_eval.go index 6f57c8072..8d6c1f04f 100644 --- a/internal/eval/store_eval.go +++ b/internal/eval/store_eval.go @@ -976,15 +976,93 @@ func evalZRANGE(args []string, store *dstore.Store) *EvalResponse { startStr := args[1] stopStr := args[2] + if !strings.HasPrefix(startStr, "(") && !strings.HasPrefix(startStr, "[") && + startStr != "-" && startStr != "+" && startStr != "-inf" && startStr != "+inf" { + if len(startStr) > 1 && startStr[0] == '0' { + return &EvalResponse{ + Result: nil, + Error: diceerrors.ErrIntegerOutOfRange, + } + } + } + if !strings.HasPrefix(stopStr, "(") && !strings.HasPrefix(stopStr, "[") && + stopStr != "-" && stopStr != "+" && stopStr != "-inf" && stopStr != "+inf" { + if len(stopStr) > 1 && stopStr[0] == '0' { + return &EvalResponse{ + Result: nil, + Error: diceerrors.ErrIntegerOutOfRange, + } + } + } + withScores := false reverse := false + byScore := false + byLex := false + limit := false + offset := 0 + count := -1 + for i := 3; i < len(args); i++ { arg := strings.ToUpper(args[i]) - if arg == WithScores { + switch arg { + case WithScores: withScores = true - } else if arg == REV { + case REV: reverse = true - } else { + case BYSCORE: + if byLex { + return &EvalResponse{ + Result: nil, + Error: diceerrors.NewErr("ERR syntax error, BYSCORE and BYLEX cannot be used together"), + } + } + byScore = true + case BYLEX: + if byScore { + return &EvalResponse{ + Result: nil, + Error: diceerrors.NewErr("ERR syntax error, BYSCORE and BYLEX cannot be used together"), + } + } + byLex = true + case LIMIT: + if !byScore && !byLex { + return &EvalResponse{ + Result: nil, + Error: diceerrors.NewErr("ERR syntax error, LIMIT is only supported in combination with either BYSCORE or BYLEX"), + } + } + if i+2 >= len(args) { + return &EvalResponse{ + Result: nil, + Error: diceerrors.ErrSyntax, + } + } + limit = true + var err error + offset, err = strconv.Atoi(args[i+1]) + if err != nil { + return &EvalResponse{ + Result: nil, + Error: diceerrors.ErrInvalidNumberFormat, + } + } + count, err = strconv.Atoi(args[i+2]) + if err != nil { + return &EvalResponse{ + Result: nil, + Error: diceerrors.ErrInvalidNumberFormat, + } + } + if offset < 0 { + return &EvalResponse{ + Result: []string{}, + Error: nil, + } + } + i += 2 + default: return &EvalResponse{ Result: nil, Error: diceerrors.ErrSyntax, @@ -992,19 +1070,37 @@ func evalZRANGE(args []string, store *dstore.Store) *EvalResponse { } } - start, err := strconv.Atoi(startStr) - if err != nil { - return &EvalResponse{ - Result: nil, - Error: diceerrors.ErrInvalidNumberFormat, + if !byLex { + var validStartStr, validStopStr string + if startStr[0] == '(' { + validStartStr = startStr[1:] + } else { + validStartStr = startStr + } + if stopStr[0] == '(' { + validStopStr = stopStr[1:] + } else { + validStopStr = stopStr } - } - stop, err := strconv.Atoi(stopStr) - if err != nil { - return &EvalResponse{ - Result: nil, - Error: diceerrors.ErrInvalidNumberFormat, + if validStartStr != "+inf" && validStartStr != "-inf" { + _, err := strconv.ParseInt(validStartStr, 10, 64) + if err != nil { + return &EvalResponse{ + Result: nil, + Error: diceerrors.ErrIntegerOutOfRange, + } + } + } + + if validStopStr != "+inf" && validStopStr != "-inf" { + _, err := strconv.ParseInt(validStopStr, 10, 64) + if err != nil { + return &EvalResponse{ + Result: nil, + Error: diceerrors.ErrIntegerOutOfRange, + } + } } } @@ -1017,7 +1113,6 @@ func evalZRANGE(args []string, store *dstore.Store) *EvalResponse { } sortedSet, errMsg := sortedset.FromObject(obj) - if errMsg != nil { return &EvalResponse{ Result: nil, @@ -1025,7 +1120,53 @@ func evalZRANGE(args []string, store *dstore.Store) *EvalResponse { } } - result := sortedSet.GetRange(start, stop, withScores, reverse) + var result []string + + if byScore { + if reverse { + startStr, stopStr = stopStr, startStr + } + start, stop, err := parseScoreRange(startStr, stopStr) + if err != nil { + return &EvalResponse{ + Result: nil, + Error: diceerrors.NewErr(err.Error()), + } + } + result = sortedSet.GetRangeByScore(start, stop, withScores, reverse, offset, count) + } else if byLex { + // TODO: PLACEHOLDER FOR BYLEX IMPLEMENTATION + } else { + start, err := strconv.ParseInt(startStr, 10, 64) + if err != nil { + return &EvalResponse{ + Result: nil, + Error: diceerrors.ErrIntegerOutOfRange, + } + } + + stop, err := strconv.ParseInt(stopStr, 10, 64) + if err != nil { + return &EvalResponse{ + Result: nil, + Error: diceerrors.ErrIntegerOutOfRange, + } + } + + result = sortedSet.GetRange(int(start), int(stop), withScores, reverse) + } + + if limit && !byScore && !byLex { + if offset >= len(result) || offset < 0 { + result = []string{} + } else { + end := offset + count + if end > len(result) || count < 0 { + end = len(result) + } + result = result[offset:end] + } + } return &EvalResponse{ Result: result, @@ -1033,6 +1174,45 @@ func evalZRANGE(args []string, store *dstore.Store) *EvalResponse { } } +func parseScoreRange(min, max string) (float64, float64, error) { + minScore := math.Inf(-1) + maxScore := math.Inf(1) + + if min != "-inf" { + var err error + if min[0] == '(' { + minScore, err = strconv.ParseFloat(min[1:], 64) + if err != nil { + return 0, 0, fmt.Errorf("ERR min or max is not a float") + } + minScore = math.Nextafter(minScore, math.Inf(1)) + } else { + minScore, err = strconv.ParseFloat(min, 64) + if err != nil { + return 0, 0, fmt.Errorf("ERR min or max is not a float") + } + } + } + + if max != "+inf" { + var err error + if max[0] == '(' { + maxScore, err = strconv.ParseFloat(max[1:], 64) + if err != nil { + return 0, 0, fmt.Errorf("ERR min or max is not a float") + } + maxScore = math.Nextafter(maxScore, math.Inf(-1)) + } else { + maxScore, err = strconv.ParseFloat(max, 64) + if err != nil { + return 0, 0, fmt.Errorf("ERR min or max is not a float") + } + } + } + + return minScore, maxScore, nil +} + // evalZREM removes the specified members from the sorted set stored at key. // Non-existing members are ignored. // Returns the number of members removed. @@ -6959,9 +7139,9 @@ func evalJSONARRINDEX(args []string, store *dstore.Store) *EvalResponse { adjustedStart, adjustedStop := adjustIndices(start, stop, length) - if adjustedStart == -1 { - arrIndexList = append(arrIndexList, -1) - continue + if adjustedStart == -1 { + arrIndexList = append(arrIndexList, -1) + continue } // Range [start, stop) : start is inclusive, stop is exclusive @@ -6984,18 +7164,18 @@ func evalJSONARRINDEX(args []string, store *dstore.Store) *EvalResponse { return makeEvalResult(arrIndexList) } -// adjustIndices adjusts the start and stop indices for array traversal. -// It handles negative indices and ensures they are within the array bounds. -func adjustIndices(start, stop, length int) (adjustedStart, adjustedStop int) { +// adjustIndices adjusts the start and stop indices for array traversal. +// It handles negative indices and ensures they are within the array bounds. +func adjustIndices(start, stop, length int) (adjustedStart, adjustedStop int) { if length == 0 { - return -1, -1 + return -1, -1 } if start < 0 { - start += length + start += length } - if stop <= 0 { - stop += length + if stop <= 0 { + stop += length } if start < 0 { start = 0 From f20078929f01fc79bd944d2f15b12489a41751af Mon Sep 17 00:00:00 2001 From: vanshavenger Date: Mon, 23 Dec 2024 03:25:59 +0530 Subject: [PATCH 3/8] Fix usage of with BYSCORE --- internal/eval/store_eval.go | 40 ++++++++++++++++++++----------------- 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/internal/eval/store_eval.go b/internal/eval/store_eval.go index 8d6c1f04f..a695f34f0 100644 --- a/internal/eval/store_eval.go +++ b/internal/eval/store_eval.go @@ -976,24 +976,7 @@ func evalZRANGE(args []string, store *dstore.Store) *EvalResponse { startStr := args[1] stopStr := args[2] - if !strings.HasPrefix(startStr, "(") && !strings.HasPrefix(startStr, "[") && - startStr != "-" && startStr != "+" && startStr != "-inf" && startStr != "+inf" { - if len(startStr) > 1 && startStr[0] == '0' { - return &EvalResponse{ - Result: nil, - Error: diceerrors.ErrIntegerOutOfRange, - } - } - } - if !strings.HasPrefix(stopStr, "(") && !strings.HasPrefix(stopStr, "[") && - stopStr != "-" && stopStr != "+" && stopStr != "-inf" && stopStr != "+inf" { - if len(stopStr) > 1 && stopStr[0] == '0' { - return &EvalResponse{ - Result: nil, - Error: diceerrors.ErrIntegerOutOfRange, - } - } - } + withScores := false reverse := false @@ -1070,6 +1053,27 @@ func evalZRANGE(args []string, store *dstore.Store) *EvalResponse { } } + if !byScore { + if !strings.HasPrefix(startStr, "(") && !strings.HasPrefix(startStr, "[") && + startStr != "-" && startStr != "+" && startStr != "-inf" && startStr != "+inf" { + if len(startStr) > 1 && startStr[0] == '0' { + return &EvalResponse{ + Result: nil, + Error: diceerrors.ErrIntegerOutOfRange, + } + } + } + if !strings.HasPrefix(stopStr, "(") && !strings.HasPrefix(stopStr, "[") && + stopStr != "-" && stopStr != "+" && stopStr != "-inf" && stopStr != "+inf" { + if len(stopStr) > 1 && stopStr[0] == '0' { + return &EvalResponse{ + Result: nil, + Error: diceerrors.ErrIntegerOutOfRange, + } + } + } + } + if !byLex { var validStartStr, validStopStr string if startStr[0] == '(' { From 46ee5e0a57f3c91fd886bdb4d11262e5a8853b94 Mon Sep 17 00:00:00 2001 From: vanshavenger Date: Mon, 23 Dec 2024 03:32:56 +0530 Subject: [PATCH 4/8] Fix: Inf constants lint error --- internal/eval/constants.go | 2 ++ internal/eval/store_eval.go | 36 +++++++++++++++++------------------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/internal/eval/constants.go b/internal/eval/constants.go index 6cfb2731e..ffacd24bf 100644 --- a/internal/eval/constants.go +++ b/internal/eval/constants.go @@ -69,4 +69,6 @@ const ( BYSCORE string = "BYSCORE" BYLEX string = "BYLEX" LIMIT string = "LIMIT" + PLUSINF string = "+inf" + MINUSINF string = "-inf" ) diff --git a/internal/eval/store_eval.go b/internal/eval/store_eval.go index a695f34f0..2455358df 100644 --- a/internal/eval/store_eval.go +++ b/internal/eval/store_eval.go @@ -976,8 +976,6 @@ func evalZRANGE(args []string, store *dstore.Store) *EvalResponse { startStr := args[1] stopStr := args[2] - - withScores := false reverse := false byScore := false @@ -1055,24 +1053,24 @@ func evalZRANGE(args []string, store *dstore.Store) *EvalResponse { if !byScore { if !strings.HasPrefix(startStr, "(") && !strings.HasPrefix(startStr, "[") && - startStr != "-" && startStr != "+" && startStr != "-inf" && startStr != "+inf" { - if len(startStr) > 1 && startStr[0] == '0' { - return &EvalResponse{ - Result: nil, - Error: diceerrors.ErrIntegerOutOfRange, + startStr != "-" && startStr != "+" && startStr != MINUSINF && startStr != PLUSINF { + if len(startStr) > 1 && startStr[0] == '0' { + return &EvalResponse{ + Result: nil, + Error: diceerrors.ErrIntegerOutOfRange, + } } } - } - if !strings.HasPrefix(stopStr, "(") && !strings.HasPrefix(stopStr, "[") && - stopStr != "-" && stopStr != "+" && stopStr != "-inf" && stopStr != "+inf" { - if len(stopStr) > 1 && stopStr[0] == '0' { - return &EvalResponse{ - Result: nil, - Error: diceerrors.ErrIntegerOutOfRange, + if !strings.HasPrefix(stopStr, "(") && !strings.HasPrefix(stopStr, "[") && + stopStr != "-" && stopStr != "+" && stopStr != MINUSINF && stopStr != PLUSINF { + if len(stopStr) > 1 && stopStr[0] == '0' { + return &EvalResponse{ + Result: nil, + Error: diceerrors.ErrIntegerOutOfRange, + } } } } - } if !byLex { var validStartStr, validStopStr string @@ -1087,7 +1085,7 @@ func evalZRANGE(args []string, store *dstore.Store) *EvalResponse { validStopStr = stopStr } - if validStartStr != "+inf" && validStartStr != "-inf" { + if validStartStr != PLUSINF && validStartStr != MINUSINF { _, err := strconv.ParseInt(validStartStr, 10, 64) if err != nil { return &EvalResponse{ @@ -1097,7 +1095,7 @@ func evalZRANGE(args []string, store *dstore.Store) *EvalResponse { } } - if validStopStr != "+inf" && validStopStr != "-inf" { + if validStopStr != PLUSINF && validStopStr != MINUSINF { _, err := strconv.ParseInt(validStopStr, 10, 64) if err != nil { return &EvalResponse{ @@ -1182,7 +1180,7 @@ func parseScoreRange(min, max string) (float64, float64, error) { minScore := math.Inf(-1) maxScore := math.Inf(1) - if min != "-inf" { + if min != MINUSINF { var err error if min[0] == '(' { minScore, err = strconv.ParseFloat(min[1:], 64) @@ -1198,7 +1196,7 @@ func parseScoreRange(min, max string) (float64, float64, error) { } } - if max != "+inf" { + if max != PLUSINF { var err error if max[0] == '(' { maxScore, err = strconv.ParseFloat(max[1:], 64) From 8164b4ba3e01c6eb1d5a375bd0559a0c52f1bc2b Mon Sep 17 00:00:00 2001 From: vanshavenger Date: Mon, 23 Dec 2024 03:33:32 +0530 Subject: [PATCH 5/8] Fix: Lint explty block --- internal/eval/store_eval.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/internal/eval/store_eval.go b/internal/eval/store_eval.go index 2455358df..d4eeebaae 100644 --- a/internal/eval/store_eval.go +++ b/internal/eval/store_eval.go @@ -1136,8 +1136,6 @@ func evalZRANGE(args []string, store *dstore.Store) *EvalResponse { } } result = sortedSet.GetRangeByScore(start, stop, withScores, reverse, offset, count) - } else if byLex { - // TODO: PLACEHOLDER FOR BYLEX IMPLEMENTATION } else { start, err := strconv.ParseInt(startStr, 10, 64) if err != nil { From 58556ad1a75a9e65a164d590e8eed210e6bf8ebd Mon Sep 17 00:00:00 2001 From: vanshavenger Date: Mon, 23 Dec 2024 03:49:37 +0530 Subject: [PATCH 6/8] Fix: Shaowing erros --- internal/eval/sortedset/sorted_set.go | 2 +- internal/eval/store_eval.go | 30 +++++++++++++-------------- 2 files changed, 15 insertions(+), 17 deletions(-) diff --git a/internal/eval/sortedset/sorted_set.go b/internal/eval/sortedset/sorted_set.go index 1429b5146..e7acdd5bd 100644 --- a/internal/eval/sortedset/sorted_set.go +++ b/internal/eval/sortedset/sorted_set.go @@ -327,7 +327,7 @@ func DeserializeSortedSet(buf *bytes.Reader) (*Set, error) { } // GetRangeByScore returns a slice of members with scores between min and max, inclusive. -func (ss *Set) GetRangeByScore(min, max float64, withScores bool, reverse bool, offset, count int) []string { +func (ss *Set) GetRangeByScore(min, max float64, withScores, reverse bool, offset, count int) []string { var result []string index := 0 returned := 0 diff --git a/internal/eval/store_eval.go b/internal/eval/store_eval.go index d4eeebaae..901236b39 100644 --- a/internal/eval/store_eval.go +++ b/internal/eval/store_eval.go @@ -1137,7 +1137,7 @@ func evalZRANGE(args []string, store *dstore.Store) *EvalResponse { } result = sortedSet.GetRangeByScore(start, stop, withScores, reverse, offset, count) } else { - start, err := strconv.ParseInt(startStr, 10, 64) + start, err := strconv.Atoi(startStr) if err != nil { return &EvalResponse{ Result: nil, @@ -1145,7 +1145,7 @@ func evalZRANGE(args []string, store *dstore.Store) *EvalResponse { } } - stop, err := strconv.ParseInt(stopStr, 10, 64) + stop, err := strconv.Atoi(stopStr) if err != nil { return &EvalResponse{ Result: nil, @@ -1153,7 +1153,7 @@ func evalZRANGE(args []string, store *dstore.Store) *EvalResponse { } } - result = sortedSet.GetRange(int(start), int(stop), withScores, reverse) + result = sortedSet.GetRange(start, stop, withScores, reverse) } if limit && !byScore && !byLex { @@ -1174,36 +1174,34 @@ func evalZRANGE(args []string, store *dstore.Store) *EvalResponse { } } -func parseScoreRange(min, max string) (float64, float64, error) { - minScore := math.Inf(-1) - maxScore := math.Inf(1) +func parseScoreRange(minStr, maxStr string) (minScore, maxScore float64, err error) { + minScore = math.Inf(-1) + maxScore = math.Inf(1) - if min != MINUSINF { - var err error - if min[0] == '(' { - minScore, err = strconv.ParseFloat(min[1:], 64) + if minStr != MINUSINF { + if minStr[0] == '(' { + minScore, err = strconv.ParseFloat(minStr[1:], 64) if err != nil { return 0, 0, fmt.Errorf("ERR min or max is not a float") } minScore = math.Nextafter(minScore, math.Inf(1)) } else { - minScore, err = strconv.ParseFloat(min, 64) + minScore, err = strconv.ParseFloat(minStr, 64) if err != nil { return 0, 0, fmt.Errorf("ERR min or max is not a float") } } } - if max != PLUSINF { - var err error - if max[0] == '(' { - maxScore, err = strconv.ParseFloat(max[1:], 64) + if maxStr != PLUSINF { + if maxStr[0] == '(' { + maxScore, err = strconv.ParseFloat(maxStr[1:], 64) if err != nil { return 0, 0, fmt.Errorf("ERR min or max is not a float") } maxScore = math.Nextafter(maxScore, math.Inf(-1)) } else { - maxScore, err = strconv.ParseFloat(max, 64) + maxScore, err = strconv.ParseFloat(maxStr, 64) if err != nil { return 0, 0, fmt.Errorf("ERR min or max is not a float") } From 827446e4dc8c4ea637f0ead1b58aca640da07abe Mon Sep 17 00:00:00 2001 From: vanshavenger Date: Mon, 23 Dec 2024 23:14:22 +0530 Subject: [PATCH 7/8] resolved shadowing errors --- internal/eval/sortedset/sorted_set.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/internal/eval/sortedset/sorted_set.go b/internal/eval/sortedset/sorted_set.go index e7acdd5bd..221538d12 100644 --- a/internal/eval/sortedset/sorted_set.go +++ b/internal/eval/sortedset/sorted_set.go @@ -327,7 +327,7 @@ func DeserializeSortedSet(buf *bytes.Reader) (*Set, error) { } // GetRangeByScore returns a slice of members with scores between min and max, inclusive. -func (ss *Set) GetRangeByScore(min, max float64, withScores, reverse bool, offset, count int) []string { +func (ss *Set) GetRangeByScore(minScore, maxScore float64, withScores, reverse bool, offset, count int) []string { var result []string index := 0 returned := 0 @@ -336,17 +336,17 @@ func (ss *Set) GetRangeByScore(min, max float64, withScores, reverse bool, offse ssi := item.(*Item) if reverse { - if ssi.Score > max { + if ssi.Score > maxScore { return true } - if ssi.Score < min { + if ssi.Score < minScore { return false } } else { - if ssi.Score < min { + if ssi.Score < minScore { return true } - if ssi.Score > max { + if ssi.Score > maxScore { return false } } From 10dedf93634869d767312218baec59ba5ced082e Mon Sep 17 00:00:00 2001 From: vanshavenger Date: Tue, 24 Dec 2024 02:39:44 +0530 Subject: [PATCH 8/8] Fix: lint errors --- internal/eval/store_eval.go | 295 ++++++++++++++++-------------------- 1 file changed, 129 insertions(+), 166 deletions(-) diff --git a/internal/eval/store_eval.go b/internal/eval/store_eval.go index 901236b39..7c5fc0ad0 100644 --- a/internal/eval/store_eval.go +++ b/internal/eval/store_eval.go @@ -966,212 +966,175 @@ func evalZCOUNT(args []string, store *dstore.Store) *EvalResponse { // The elements are considered to be ordered from the lowest to the highest score. func evalZRANGE(args []string, store *dstore.Store) *EvalResponse { if len(args) < 3 { - return &EvalResponse{ - Result: nil, - Error: diceerrors.ErrWrongArgumentCount("ZRANGE"), + return &EvalResponse{Result: nil, Error: diceerrors.ErrWrongArgumentCount("ZRANGE")} + } + + opts := parseOptions(args[3:]) + if opts.error != nil { + return &EvalResponse{Result: nil, Error: opts.error} + } + + if opts.byScore && opts.byLex { + return &EvalResponse{Result: nil, Error: diceerrors.ErrSyntax} + } + + if opts.limit && !opts.byScore && !opts.byLex { + return &EvalResponse{Result: nil, Error: diceerrors.NewErr("ERR syntax error, LIMIT is only supported in combination with either BYSCORE or BYLEX")} + } + + startStr, stopStr := args[1], args[2] + if !opts.byScore { + if err := validateRange(startStr, stopStr, opts.byScore, opts.byLex); err != nil { + return &EvalResponse{Result: nil, Error: err} + } + } else { + _, _, err := parseScoreRange(startStr, stopStr) + if err != nil { + return &EvalResponse{Result: nil, Error: err} } } - key := args[0] - startStr := args[1] - stopStr := args[2] + obj := store.Get(args[0]) + if obj == nil { + return &EvalResponse{Result: []string{}, Error: nil} + } - withScores := false - reverse := false - byScore := false - byLex := false - limit := false - offset := 0 - count := -1 + sortedSet, errMsg := sortedset.FromObject(obj) + if errMsg != nil { + return &EvalResponse{Result: nil, Error: diceerrors.ErrWrongTypeOperation} + } - for i := 3; i < len(args); i++ { - arg := strings.ToUpper(args[i]) - switch arg { + result := getRange(sortedSet, startStr, stopStr, opts) + + if opts.limit && !opts.byScore && !opts.byLex && opts.offset >= 0 { + if opts.offset < len(result) { + end := opts.offset + opts.count + if end > len(result) || opts.count < 0 { + end = len(result) + } + result = result[opts.offset:end] + } else { + result = []string{} + } + } + + return &EvalResponse{Result: result, Error: nil} +} + +type rangeOptions struct { + withScores bool + reverse bool + byScore bool + byLex bool + limit bool + offset int + count int + error error +} + +func parseOptions(args []string) rangeOptions { + opts := rangeOptions{count: -1} + + for i := 0; i < len(args); i++ { + switch strings.ToUpper(args[i]) { case WithScores: - withScores = true + opts.withScores = true case REV: - reverse = true + opts.reverse = true case BYSCORE: - if byLex { - return &EvalResponse{ - Result: nil, - Error: diceerrors.NewErr("ERR syntax error, BYSCORE and BYLEX cannot be used together"), - } - } - byScore = true + opts.byScore = true case BYLEX: - if byScore { - return &EvalResponse{ - Result: nil, - Error: diceerrors.NewErr("ERR syntax error, BYSCORE and BYLEX cannot be used together"), - } - } - byLex = true + opts.byLex = true case LIMIT: - if !byScore && !byLex { - return &EvalResponse{ - Result: nil, - Error: diceerrors.NewErr("ERR syntax error, LIMIT is only supported in combination with either BYSCORE or BYLEX"), - } - } if i+2 >= len(args) { - return &EvalResponse{ - Result: nil, - Error: diceerrors.ErrSyntax, - } + opts.error = diceerrors.ErrSyntax + return opts } - limit = true + var err error - offset, err = strconv.Atoi(args[i+1]) + opts.offset, err = strconv.Atoi(args[i+1]) if err != nil { - return &EvalResponse{ - Result: nil, - Error: diceerrors.ErrInvalidNumberFormat, - } + opts.error = diceerrors.ErrIntegerOutOfRange + return opts } - count, err = strconv.Atoi(args[i+2]) + + opts.count, err = strconv.Atoi(args[i+2]) if err != nil { - return &EvalResponse{ - Result: nil, - Error: diceerrors.ErrInvalidNumberFormat, - } - } - if offset < 0 { - return &EvalResponse{ - Result: []string{}, - Error: nil, - } + opts.error = diceerrors.ErrIntegerOutOfRange + return opts } + + opts.limit = true i += 2 default: - return &EvalResponse{ - Result: nil, - Error: diceerrors.ErrSyntax, - } + opts.error = diceerrors.ErrSyntax + return opts } } - if !byScore { - if !strings.HasPrefix(startStr, "(") && !strings.HasPrefix(startStr, "[") && - startStr != "-" && startStr != "+" && startStr != MINUSINF && startStr != PLUSINF { - if len(startStr) > 1 && startStr[0] == '0' { - return &EvalResponse{ - Result: nil, - Error: diceerrors.ErrIntegerOutOfRange, - } - } - } - if !strings.HasPrefix(stopStr, "(") && !strings.HasPrefix(stopStr, "[") && - stopStr != "-" && stopStr != "+" && stopStr != MINUSINF && stopStr != PLUSINF { - if len(stopStr) > 1 && stopStr[0] == '0' { - return &EvalResponse{ - Result: nil, - Error: diceerrors.ErrIntegerOutOfRange, - } - } - } + return opts +} + +func validateRange(start, stop string, byScore, byLex bool) error { + if !byScore && !isValidRangeFormat(start, stop) { + return diceerrors.ErrIntegerOutOfRange } if !byLex { - var validStartStr, validStopStr string - if startStr[0] == '(' { - validStartStr = startStr[1:] - } else { - validStartStr = startStr - } - if stopStr[0] == '(' { - validStopStr = stopStr[1:] - } else { - validStopStr = stopStr + if !isValidNumber(stripPrefix(start)) || !isValidNumber(stripPrefix(stop)) { + return diceerrors.ErrIntegerOutOfRange } + } - if validStartStr != PLUSINF && validStartStr != MINUSINF { - _, err := strconv.ParseInt(validStartStr, 10, 64) - if err != nil { - return &EvalResponse{ - Result: nil, - Error: diceerrors.ErrIntegerOutOfRange, - } - } - } + return nil +} - if validStopStr != PLUSINF && validStopStr != MINUSINF { - _, err := strconv.ParseInt(validStopStr, 10, 64) - if err != nil { - return &EvalResponse{ - Result: nil, - Error: diceerrors.ErrIntegerOutOfRange, - } - } - } - } +func isValidRangeFormat(start, stop string) bool { + return isSpecialRange(start) && isSpecialRange(stop) +} - obj := store.Get(key) - if obj == nil { - return &EvalResponse{ - Result: []string{}, - Error: nil, - } - } +func isSpecialRange(s string) bool { + return strings.HasPrefix(s, "(") || strings.HasPrefix(s, "[") || + s == "-" || s == "+" || s == MINUSINF || s == PLUSINF || + !(len(s) > 1 && s[0] == '0') +} - sortedSet, errMsg := sortedset.FromObject(obj) - if errMsg != nil { - return &EvalResponse{ - Result: nil, - Error: diceerrors.ErrWrongTypeOperation, - } +func stripPrefix(s string) string { + if strings.HasPrefix(s, "(") || strings.HasPrefix(s, "[") { + return s[1:] } + return s +} - var result []string +func isValidNumber(s string) bool { + if s == PLUSINF || s == MINUSINF { + return true + } + _, err := strconv.ParseInt(s, 10, 64) + return err == nil +} - if byScore { - if reverse { - startStr, stopStr = stopStr, startStr - } - start, stop, err := parseScoreRange(startStr, stopStr) - if err != nil { - return &EvalResponse{ - Result: nil, - Error: diceerrors.NewErr(err.Error()), - } - } - result = sortedSet.GetRangeByScore(start, stop, withScores, reverse, offset, count) - } else { - start, err := strconv.Atoi(startStr) - if err != nil { - return &EvalResponse{ - Result: nil, - Error: diceerrors.ErrIntegerOutOfRange, - } +func getRange(set *sortedset.Set, start, stop string, opts rangeOptions) []string { + if opts.byScore { + if opts.reverse { + start, stop = stop, start } - - stop, err := strconv.Atoi(stopStr) + rangeStart, rangeStop, err := parseScoreRange(start, stop) if err != nil { - return &EvalResponse{ - Result: nil, - Error: diceerrors.ErrIntegerOutOfRange, - } + return []string{} } - - result = sortedSet.GetRange(start, stop, withScores, reverse) + return set.GetRangeByScore(rangeStart, rangeStop, opts.withScores, opts.reverse, opts.offset, opts.count) } - if limit && !byScore && !byLex { - if offset >= len(result) || offset < 0 { - result = []string{} - } else { - end := offset + count - if end > len(result) || count < 0 { - end = len(result) - } - result = result[offset:end] - } + startIdx, err := strconv.Atoi(start) + if err != nil { + return []string{} } - - return &EvalResponse{ - Result: result, - Error: nil, + stopIdx, err := strconv.Atoi(stop) + if err != nil { + return []string{} } + return set.GetRange(startIdx, stopIdx, opts.withScores, opts.reverse) } func parseScoreRange(minStr, maxStr string) (minScore, maxScore float64, err error) {