Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for LCS Command #2480

Merged
merged 18 commits into from
Mar 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
177 changes: 173 additions & 4 deletions command.go
Original file line number Diff line number Diff line change
Expand Up @@ -3993,9 +3993,178 @@ func (cmd *FunctionListCmd) readFunctions(rd *proto.Reader) ([]Function, error)
return functions, nil
}

type FilterBy struct {
Module string
ACLCat string
Pattern string
//------------------------------------------------------------------------------

// LCSQuery is a parameter used for the LCS command
type LCSQuery struct {
Key1 string
Key2 string
Len bool
Idx bool
MinMatchLen int
WithMatchLen bool
}

// LCSMatch is the result set of the LCS command.
type LCSMatch struct {
MatchString string
Matches []LCSMatchedPosition
Len int64
}

type LCSMatchedPosition struct {
Key1 LCSPosition
Key2 LCSPosition

// only for withMatchLen is true
MatchLen int64
}

type LCSPosition struct {
Start int64
End int64
}

type LCSCmd struct {
baseCmd

// 1: match string
// 2: match len
// 3: match idx LCSMatch
readType uint8
val *LCSMatch
}

func NewLCSCmd(ctx context.Context, q *LCSQuery) *LCSCmd {
args := make([]interface{}, 3, 7)
args[0] = "lcs"
args[1] = q.Key1
args[2] = q.Key2

cmd := &LCSCmd{readType: 1}
if q.Len {
cmd.readType = 2
args = append(args, "len")
} else if q.Idx {
cmd.readType = 3
args = append(args, "idx")
if q.MinMatchLen != 0 {
args = append(args, "minmatchlen", q.MinMatchLen)
}
if q.WithMatchLen {
args = append(args, "withmatchlen")
}
}
cmd.baseCmd = baseCmd{
ctx: ctx,
args: args,
}

return cmd
}

func (cmd *LCSCmd) SetVal(val *LCSMatch) {
cmd.val = val
}

func (cmd *LCSCmd) String() string {
return cmdString(cmd, cmd.val)
}

func (cmd *LCSCmd) Val() *LCSMatch {
return cmd.val
}

func (cmd *LCSCmd) Result() (*LCSMatch, error) {
return cmd.val, cmd.err
}

func (cmd *LCSCmd) readReply(rd *proto.Reader) (err error) {
lcs := &LCSMatch{}
switch cmd.readType {
case 1:
// match string
if lcs.MatchString, err = rd.ReadString(); err != nil {
return err
}
case 2:
// match len
if lcs.Len, err = rd.ReadInt(); err != nil {
return err
}
case 3:
// read LCSMatch
if err = rd.ReadFixedMapLen(2); err != nil {
return err
}

// read matches or len field
for i := 0; i < 2; i++ {
key, err := rd.ReadString()
if err != nil {
return err
}

switch key {
case "matches":
// read array of matched positions
if lcs.Matches, err = cmd.readMatchedPositions(rd); err != nil {
return err
}
case "len":
// read match length
if lcs.Len, err = rd.ReadInt(); err != nil {
return err
}
}
}
}

cmd.val = lcs
return nil
}

func (cmd *LCSCmd) readMatchedPositions(rd *proto.Reader) ([]LCSMatchedPosition, error) {
n, err := rd.ReadArrayLen()
if err != nil {
return nil, err
}

positions := make([]LCSMatchedPosition, n)
for i := 0; i < n; i++ {
pn, err := rd.ReadArrayLen()
if err != nil {
return nil, err
}

if positions[i].Key1, err = cmd.readPosition(rd); err != nil {
return nil, err
}
if positions[i].Key2, err = cmd.readPosition(rd); err != nil {
return nil, err
}

// read match length if WithMatchLen is true
if pn > 2 {
if positions[i].MatchLen, err = rd.ReadInt(); err != nil {
return nil, err
}
}
}

return positions, nil
}

func (cmd *LCSCmd) readPosition(rd *proto.Reader) (pos LCSPosition, err error) {
if err = rd.ReadFixedArrayLen(2); err != nil {
return pos, err
}
if pos.Start, err = rd.ReadInt(); err != nil {
return pos, err
}
if pos.End, err = rd.ReadInt(); err != nil {
return pos, err
}

return pos, nil
}
14 changes: 14 additions & 0 deletions commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,7 @@ type Cmdable interface {
BLMPop(ctx context.Context, timeout time.Duration, direction string, count int64, keys ...string) *KeyValuesCmd
BRPop(ctx context.Context, timeout time.Duration, keys ...string) *StringSliceCmd
BRPopLPush(ctx context.Context, source, destination string, timeout time.Duration) *StringCmd
LCS(ctx context.Context, q *LCSQuery) *LCSCmd
LIndex(ctx context.Context, key string, index int64) *StringCmd
LInsert(ctx context.Context, key, op string, pivot, value interface{}) *IntCmd
LInsertBefore(ctx context.Context, key string, pivot, value interface{}) *IntCmd
Expand Down Expand Up @@ -543,6 +544,13 @@ func (c cmdable) Command(ctx context.Context) *CommandsInfoCmd {
return cmd
}

// FilterBy is used for the `CommandList` command parameter.
type FilterBy struct {
Module string
ACLCat string
Pattern string
}

func (c cmdable) CommandList(ctx context.Context, filter *FilterBy) *StringSliceCmd {
args := make([]interface{}, 0, 5)
args = append(args, "command", "list")
Expand Down Expand Up @@ -1524,6 +1532,12 @@ func (c cmdable) BRPopLPush(ctx context.Context, source, destination string, tim
return cmd
}

func (c cmdable) LCS(ctx context.Context, q *LCSQuery) *LCSCmd {
cmd := NewLCSCmd(ctx, q)
_ = c(ctx, cmd)
return cmd
}

func (c cmdable) LIndex(ctx context.Context, key string, index int64) *StringCmd {
cmd := NewStringCmd(ctx, "lindex", key, index)
_ = c(ctx, cmd)
Expand Down
80 changes: 80 additions & 0 deletions commands_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2251,6 +2251,86 @@ var _ = Describe("Commands", func() {
Expect(v).To(Equal("c"))
})

It("should LCS", func() {
err := client.MSet(ctx, "key1", "ohmytext", "key2", "mynewtext").Err()
Expect(err).NotTo(HaveOccurred())

lcs, err := client.LCS(ctx, &redis.LCSQuery{
Key1: "key1",
Key2: "key2",
}).Result()

Expect(err).NotTo(HaveOccurred())
Expect(lcs.MatchString).To(Equal("mytext"))

lcs, err = client.LCS(ctx, &redis.LCSQuery{
Key1: "nonexistent_key1",
Key2: "key2",
}).Result()

Expect(err).NotTo(HaveOccurred())
Expect(lcs.MatchString).To(Equal(""))

lcs, err = client.LCS(ctx, &redis.LCSQuery{
Key1: "key1",
Key2: "key2",
Len: true,
}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(lcs.MatchString).To(Equal(""))
Expect(lcs.Len).To(Equal(int64(6)))

lcs, err = client.LCS(ctx, &redis.LCSQuery{
Key1: "key1",
Key2: "key2",
Idx: true,
}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(lcs.MatchString).To(Equal(""))
Expect(lcs.Len).To(Equal(int64(6)))
Expect(lcs.Matches).To(Equal([]redis.LCSMatchedPosition{
{
Key1: redis.LCSPosition{Start: 4, End: 7},
Key2: redis.LCSPosition{Start: 5, End: 8},
MatchLen: 0,
},
{
Key1: redis.LCSPosition{Start: 2, End: 3},
Key2: redis.LCSPosition{Start: 0, End: 1},
MatchLen: 0,
},
}))

lcs, err = client.LCS(ctx, &redis.LCSQuery{
Key1: "key1",
Key2: "key2",
Idx: true,
SoulPancake marked this conversation as resolved.
Show resolved Hide resolved
MinMatchLen: 3,
WithMatchLen: true,
}).Result()
Expect(err).NotTo(HaveOccurred())
Expect(lcs.MatchString).To(Equal(""))
Expect(lcs.Len).To(Equal(int64(6)))
Expect(lcs.Matches).To(Equal([]redis.LCSMatchedPosition{
{
Key1: redis.LCSPosition{Start: 4, End: 7},
Key2: redis.LCSPosition{Start: 5, End: 8},
MatchLen: 4,
},
}))

_, err = client.Set(ctx, "keywithstringvalue", "golang", 0).Result()
Expect(err).NotTo(HaveOccurred())
_, err = client.LPush(ctx, "keywithnonstringvalue", "somevalue").Result()
Expect(err).NotTo(HaveOccurred())
_, err = client.LCS(ctx, &redis.LCSQuery{
Key1: "keywithstringvalue",
Key2: "keywithnonstringvalue",
}).Result()
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(Equal("ERR The specified keys must contain string values"))
})

It("should LIndex", func() {
lPush := client.LPush(ctx, "list", "World")
Expect(lPush.Err()).NotTo(HaveOccurred())
Expand Down