Skip to content

Commit 89f35f2

Browse files
Merge pull request #204 from codecrafters-io/eddie/zsets
Sorted Sets Extension
2 parents bf9b9f4 + eaa7f69 commit 89f35f2

30 files changed

+5979
-1778
lines changed

Makefile

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,11 @@ test_pubsub_with_redis: build
7777
CODECRAFTERS_TEST_CASES_JSON="[{\"slug\":\"mx3\",\"tester_log_prefix\":\"stage-601\",\"title\":\"Stage #601: SUBSCRIBE-1\"},{\"slug\":\"zc8\",\"tester_log_prefix\":\"stage-602\",\"title\":\"Stage #602: SUBSCRIBE-2\"}, {\"slug\":\"aw8\",\"tester_log_prefix\":\"stage-603\",\"title\":\"Stage #603: SUBSCRIBE-3\"}, {\"slug\":\"lf1\",\"tester_log_prefix\":\"stage-604\",\"title\":\"Stage #604: SUBSCRIBE-4\"}, {\"slug\":\"hf2\",\"tester_log_prefix\":\"stage-605\",\"title\":\"Stage #605: PUBLISH-1\"}, {\"slug\":\"dn4\",\"tester_log_prefix\":\"stage-606\",\"title\":\"Stage #606: PUBLISH-2\"}, {\"slug\":\"ze9\",\"tester_log_prefix\":\"stage-607\",\"title\":\"Stage #607: UNSUBSCRIBE\"}]" \
7878
dist/main.out
7979

80+
test_zset_with_redis: build
81+
CODECRAFTERS_REPOSITORY_DIR=./internal/test_helpers/pass_all \
82+
CODECRAFTERS_TEST_CASES_JSON="[{\"slug\":\"ct1\",\"tester_log_prefix\":\"stage-701\",\"title\":\"Stage #701: ZADD-1\"},{\"slug\":\"hf1\",\"tester_log_prefix\":\"stage-702\",\"title\":\"Stage #702: ZADD-2\"}, {\"slug\":\"lg6\",\"tester_log_prefix\":\"stage-703\",\"title\":\"Stage #703: ZRANK\"}, {\"slug\":\"ic1\",\"tester_log_prefix\":\"stage-704\",\"title\":\"Stage #704: ZRANGE-1\"}, {\"slug\":\"bj4\",\"tester_log_prefix\":\"stage-705\",\"title\":\"Stage #705: ZRANGE-2\"}, {\"slug\":\"kn4\",\"tester_log_prefix\":\"stage-706\",\"title\":\"Stage #706: ZCARD\"}, {\"slug\":\"gd7\",\"tester_log_prefix\":\"stage-707\",\"title\":\"Stage #707: ZSCORE\"}, {\"slug\":\"sq7\",\"tester_log_prefix\":\"stage-708\",\"title\":\"Stage #708: ZREM\"} ]" \
83+
dist/main.out
84+
8085
test_all_with_redis:
8186
make test_base_with_redis || true
8287
make test_repl_with_redis || true
@@ -85,6 +90,7 @@ test_all_with_redis:
8590
make test_txn_with_redis || true
8691
make test_list_with_redis || true
8792
make test_pubsub_with_redis || true
93+
make test_zset_with_redis || true
8894

8995
setup:
9096
echo "Setting up redis-tester prerequisites for Linux"

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ go 1.24
55
toolchain go1.24.2
66

77
require (
8-
github.com/codecrafters-io/tester-utils v0.4.6
8+
github.com/codecrafters-io/tester-utils v0.4.7
99
github.com/hdt3213/rdb v1.2.0
1010
github.com/stretchr/testify v1.10.0
1111
github.com/tidwall/pretty v1.2.1

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F
66
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
77
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
88
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
9-
github.com/codecrafters-io/tester-utils v0.4.6 h1:u07xobjZWul2J3R+WaDxq6KbzZ8l8y3ThfE6fzN4HMY=
10-
github.com/codecrafters-io/tester-utils v0.4.6/go.mod h1:Fyrv4IebzjWtvKfpYf8ooYDoOtjYe2qx8bV7KAJpX+w=
9+
github.com/codecrafters-io/tester-utils v0.4.7 h1:M6bNk9Pr8tg3AyoU+OSQqV9poND6cmJSxnA8paLtdC4=
10+
github.com/codecrafters-io/tester-utils v0.4.7/go.mod h1:Fyrv4IebzjWtvKfpYf8ooYDoOtjYe2qx8bV7KAJpX+w=
1111
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
1212
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
1313
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
package data_structures
2+
3+
import (
4+
"sort"
5+
6+
testerutils_random "github.com/codecrafters-io/tester-utils/random"
7+
)
8+
9+
type SortedSetMember struct {
10+
Name string
11+
Score float64
12+
}
13+
14+
// SortedSet is a data structure that maintains its elements sorted by score.
15+
// If multiple elements have the same score, they are ordered lexicographically.
16+
type SortedSet struct {
17+
members []SortedSetMember
18+
}
19+
20+
func NewSortedSet() *SortedSet {
21+
return &SortedSet{}
22+
}
23+
24+
func (ss *SortedSet) AddMember(m SortedSetMember) *SortedSet {
25+
ss.members = append(ss.members, m)
26+
ss.sort()
27+
return ss
28+
}
29+
30+
func (ss *SortedSet) RemoveMember(name string) *SortedSet {
31+
for i, m := range ss.members {
32+
if m.Name == name {
33+
ss.members = append(ss.members[:i], ss.members[i+1:]...)
34+
return ss
35+
}
36+
}
37+
return ss
38+
}
39+
40+
func (ss *SortedSet) Size() int {
41+
return len(ss.members)
42+
}
43+
44+
// GetMembers returns the a copy of all the members
45+
func (ss *SortedSet) GetMembers() []SortedSetMember {
46+
members := make([]SortedSetMember, len(ss.members))
47+
copy(members, ss.members)
48+
return members
49+
}
50+
51+
// GetMemberNames returns a slice containing all member names
52+
func (ss *SortedSet) GetMemberNames() []string {
53+
memberNames := make([]string, len(ss.members))
54+
for i, m := range ss.members {
55+
memberNames[i] = m.Name
56+
}
57+
return memberNames
58+
}
59+
60+
type SortedSetMemberGenerationOption struct {
61+
Count int // Total number of members to generate
62+
SameScoreCount int // Number of members with same score (for testing lexicographic sorting)
63+
}
64+
65+
func GenerateSortedSetWithRandomMembers(option SortedSetMemberGenerationOption) *SortedSet {
66+
count := option.Count
67+
sameScoreCount := min(option.SameScoreCount, count)
68+
differentScoresCount := count - sameScoreCount
69+
70+
ss := NewSortedSet()
71+
72+
memberNames := testerutils_random.RandomWords(count)
73+
74+
// generate members with different scores
75+
for i := range differentScoresCount {
76+
score := GetRandomSortedSetScore()
77+
ss.AddMember(SortedSetMember{
78+
Name: memberNames[i],
79+
Score: score,
80+
})
81+
}
82+
83+
// generate members with same score
84+
baseScore := GetRandomSortedSetScore()
85+
for i := range sameScoreCount {
86+
ss.AddMember(SortedSetMember{
87+
Name: memberNames[differentScoresCount+i],
88+
Score: baseScore,
89+
})
90+
}
91+
92+
return ss
93+
}
94+
95+
// GetRandomSortedSetScore returns a random value of score for a sorted set
96+
func GetRandomSortedSetScore() float64 {
97+
return testerutils_random.RandomFloat64(1, 100)
98+
}
99+
100+
// sort orders members by ascending value of score
101+
// if scores are same, the members are sorted lexicographically
102+
func (ss *SortedSet) sort() {
103+
sort.Slice(ss.members, func(i, j int) bool {
104+
if ss.members[i].Score != ss.members[j].Score {
105+
return ss.members[i].Score < ss.members[j].Score
106+
}
107+
return ss.members[i].Name < ss.members[j].Name
108+
})
109+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package resp_assertions
2+
3+
import (
4+
"fmt"
5+
"math"
6+
"strconv"
7+
8+
resp_value "github.com/codecrafters-io/redis-tester/internal/resp/value"
9+
)
10+
11+
type FloatingPointBulkStringAssertion struct {
12+
ExpectedValue float64
13+
Tolerance float64
14+
}
15+
16+
func NewFloatingPointBulkStringAssertion(expectedValue float64, tolerance float64) RESPAssertion {
17+
return FloatingPointBulkStringAssertion{
18+
ExpectedValue: expectedValue,
19+
Tolerance: tolerance,
20+
}
21+
}
22+
23+
func (a FloatingPointBulkStringAssertion) Run(value resp_value.Value) error {
24+
if value.Type != resp_value.BULK_STRING {
25+
return fmt.Errorf("Expected bulk string, got %s", value.Type)
26+
}
27+
28+
stringValue := value.String()
29+
floatValue, err := strconv.ParseFloat(stringValue, 64)
30+
if err != nil {
31+
return fmt.Errorf("Expected %q to be a floating point number", stringValue)
32+
}
33+
34+
diff := math.Abs(floatValue - a.ExpectedValue)
35+
36+
if diff > a.Tolerance {
37+
expectedStr := fmt.Sprintf("%g ± %g", a.ExpectedValue, a.Tolerance)
38+
return fmt.Errorf("Expected %s, got %g", expectedStr, floatValue)
39+
}
40+
41+
return nil
42+
}

internal/stages_test.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,13 @@ func TestStages(t *testing.T) {
131131
StdoutFixturePath: "./test_helpers/fixtures/pubsub/pass",
132132
NormalizeOutputFunc: normalizeTesterOutput,
133133
},
134+
"zset_pass": {
135+
UntilStageSlug: "sq7",
136+
CodePath: "./test_helpers/pass_all",
137+
ExpectedExitCode: 0,
138+
StdoutFixturePath: "./test_helpers/fixtures/zset/pass",
139+
NormalizeOutputFunc: normalizeTesterOutput,
140+
},
134141
}
135142

136143
tester_utils_testing.TestTesterOutput(t, testerDefinition, testCases)

internal/test_cases/receive_value_test_case.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,6 @@ func (t *ReceiveValueTestCase) Assert(client *instrumented_resp_connection.Instr
4747
}
4848
}
4949

50-
client.GetLogger().Successf("Received %s", t.ActualValue.FormattedString())
50+
client.GetLogger().Successf("✔︎ Received %s", t.ActualValue.FormattedString())
5151
return nil
5252
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package test_cases
2+
3+
import (
4+
"strconv"
5+
6+
"github.com/codecrafters-io/redis-tester/internal/data_structures"
7+
"github.com/codecrafters-io/redis-tester/internal/instrumented_resp_connection"
8+
"github.com/codecrafters-io/redis-tester/internal/resp_assertions"
9+
"github.com/codecrafters-io/tester-utils/logger"
10+
)
11+
12+
type ZaddTestCase struct {
13+
Key string
14+
Member data_structures.SortedSetMember
15+
ExpectedAddedMembersCount int
16+
}
17+
18+
func (t *ZaddTestCase) Run(client *instrumented_resp_connection.InstrumentedRespConnection, logger *logger.Logger) error {
19+
scoreStr := strconv.FormatFloat(t.Member.Score, 'f', -1, 64)
20+
sendCommandTestCase := SendCommandTestCase{
21+
Command: "ZADD",
22+
Args: []string{t.Key, scoreStr, t.Member.Name},
23+
Assertion: resp_assertions.NewIntegerAssertion(t.ExpectedAddedMembersCount),
24+
}
25+
return sendCommandTestCase.Run(client, logger)
26+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package test_cases
2+
3+
import (
4+
"strconv"
5+
6+
"github.com/codecrafters-io/redis-tester/internal/instrumented_resp_connection"
7+
"github.com/codecrafters-io/redis-tester/internal/resp_assertions"
8+
"github.com/codecrafters-io/tester-utils/logger"
9+
)
10+
11+
type ZrangeTestCase struct {
12+
Key string
13+
StartIndex int
14+
EndIndex int
15+
ExpectedMemberNames []string
16+
}
17+
18+
func (t *ZrangeTestCase) Run(client *instrumented_resp_connection.InstrumentedRespConnection, logger *logger.Logger) error {
19+
startIdxStr := strconv.Itoa(t.StartIndex)
20+
endIdxStr := strconv.Itoa(t.EndIndex)
21+
sendCommandTestCase := SendCommandTestCase{
22+
Command: "ZRANGE",
23+
Args: []string{t.Key, startIdxStr, endIdxStr},
24+
Assertion: resp_assertions.NewOrderedStringArrayAssertion(t.ExpectedMemberNames),
25+
}
26+
27+
return sendCommandTestCase.Run(client, logger)
28+
}

internal/test_helpers/course_definition.yml

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,17 @@ extensions:
123123
[subscribe-command]: https://redis.io/docs/latest/commands/subscribe/
124124
[publish-command]: https://redis.io/docs/latest/commands/publish/
125125
126+
- slug: "sorted-sets"
127+
name: "Sorted Sets"
128+
description_markdown : |-
129+
In this challenge extension you'll add support for [Sorted Sets (zsets)][redis-zset] to your Redis implementation.
130+
131+
Along the way, you'll learn commands like [ZADD][zadd-command], [ZRANGE][zrange-command], and more.
132+
133+
[redis-zset]: https://redis.io/docs/latest/develop/data-types/sorted-sets/
134+
[zadd-command]: https://redis.io/docs/latest/commands/zadd/
135+
[zrange-command]: https://redis.io/docs/latest/commands/zrange/
136+
126137
stages:
127138
- slug: "jm1"
128139
concept_slugs:
@@ -3596,4 +3607,61 @@ stages:
35963607
primary_extension_slug: "pub-sub"
35973608
name: "Unsubscribe"
35983609
difficulty: medium
3599-
marketing_md: In this stage, you'll add support for the `UNSUBSCRIBE command`, which is used to unsubscribe from a channel.
3610+
marketing_md: In this stage, you'll add support for the `UNSUBSCRIBE command`, which is used to unsubscribe from a channel.
3611+
3612+
# Sorted Sets
3613+
- slug: "ct1"
3614+
primary_extension_slug: "sorted-sets"
3615+
name: "Create a sorted set"
3616+
difficulty: easy
3617+
marketing_md: |
3618+
In this stage, you'll add support for creating a [Redis sorted set](https://redis.io/docs/latest/develop/data-types/sorted-sets/) using the `ZADD` command.
3619+
3620+
- slug: "hf1"
3621+
primary_extension_slug: "sorted-sets"
3622+
name: "Add members"
3623+
difficulty: easy
3624+
marketing_md: |
3625+
In this stage, you'll add support for adding elements to an existing sorted set.
3626+
3627+
- slug: "lg6"
3628+
primary_extension_slug: "sorted-sets"
3629+
name: "Retrieve member rank"
3630+
difficulty: medium
3631+
marketing_md: |
3632+
In this stage, you'll add support for retrieving the rank of a sorted set member.
3633+
3634+
- slug: "ic1"
3635+
primary_extension_slug: "sorted-sets"
3636+
name: "List zset members"
3637+
difficulty: easy
3638+
marketing_md: |
3639+
In this stage, you'll add support for listing the members of a sorted set using the `ZRANGE` command.
3640+
3641+
- slug: "bj4"
3642+
primary_extension_slug: "sorted-sets"
3643+
name: "ZRANGE with negative indexes"
3644+
difficulty: easy
3645+
marketing_md: |
3646+
In this stage, you'll add support for negative indexes for the `ZRANGE` command.
3647+
3648+
- slug: "kn4"
3649+
primary_extension_slug: "sorted-sets"
3650+
name: "Count zset members"
3651+
difficulty: easy
3652+
marketing_md: |
3653+
In this stage, you'll add support for counting the number of members in a sorted set using the `ZCARD` command.
3654+
3655+
- slug: "gd7"
3656+
primary_extension_slug: "sorted-sets"
3657+
name: "Retrieve member score"
3658+
difficulty: medium
3659+
marketing_md: |
3660+
In this stage, you'll add support for retrieving the score of a sorted set member using the `ZSCORE` command.
3661+
3662+
- slug: "sq7"
3663+
primary_extension_slug: "sorted-sets"
3664+
name: "Remove a member"
3665+
difficulty: easy
3666+
marketing_md: |
3667+
In this stage, you'll add support for removing a single member of a sorted set using the `ZREM` command.

0 commit comments

Comments
 (0)