diff --git a/README.md b/README.md index 2416b4e4..839a2a61 100644 --- a/README.md +++ b/README.md @@ -216,6 +216,8 @@ Supported helpers for tuples: - [UnzipBy2 -> UnzipBy9](#unzipby2---unzipby9) - [CrossJoin2 -> CrossJoin2](#crossjoin2---crossjoin9) - [CrossJoinBy2 -> CrossJoinBy2](#crossjoinby2---crossjoinby9) +- [NestJoin](#NestJoin) +- [LeftJoin](#LeftJoin) Supported helpers for time and duration: @@ -2607,6 +2609,97 @@ result, err := lo.CrossJoinByErr2([]string{"hello", "john"}, []int{1, 2}, func(a // []string(nil), error("john not allowed") ``` +### NestJoin + +```go +type User struct { + Id uint64 + Name string +} + +type Book struct { + Id uint64 + Title string + Author uint64 // = User.Id +} + +type BookWithUser struct { + Book + UserName string +} + +user1 := User{ + Id: 1, + Name: "tt", +} +book1 := Book{ + Id: 1, + Title: "Watcher from mikey", + Author: 1, +} +UserBookMatcher := func(j User, k Book) bool { + return j.Id == k.Author +} + +result := NestJoin([]User{user1}, []Book{book1}, UserBookMatcher, func(j User, k Book) BookWithUser { + return BookWithUser{ + Book: k, + UserName: j.Name, + } +}) +fmt.Printf("%v", result) +// Output: [{{1 Watcher from mikey 1} tt}] +``` + +### LeftJoin + +```go +type User struct { + Id uint64 + Name string +} + +type Book struct { + Id uint64 + Title string + Author uint64 // = User.Id +} + +type BookWithUser struct { + Book + UserName string +} + +user1 := User{ + Id: 1, + Name: "tt", +} +user2 := User{ + Id: 2, + Name: "t2", +} +book1 := Book{ + Id: 1, + Title: "Watcher from mikey", + Author: 1, +} +lk := func(item User) uint64 { + return item.Id +} +rk := func(item Book) uint64 { + return item.Author +} + +result := LeftJoin([]User{user1, user2}, []Book{book1}, lk, rk, func(j User, k Book) BookWithUser { + return BookWithUser{ + Book: k, + UserName: j.Name, + } +}) +fmt.Printf("%v", result) +// Output: [{{1 Watcher from mikey 1} tt} {{0 0} t2}] +``` + ### Duration Returns the time taken to execute a function. diff --git a/tuples.go b/tuples.go index c4360d38..b9a06acb 100644 --- a/tuples.go +++ b/tuples.go @@ -1870,3 +1870,76 @@ func CrossJoinByErr9[A, B, C, D, E, F, G, H, I, Out any](listA []A, listB []B, l return result, nil } + +// NestJoin executes a condition join (Theta Join) between two slices using a +// Nested Loop Join algorithm. +// +// It iterates through every element in the 'left' slice and compares it with +// every element in the 'right' slice using the provided 'match' predicate. +// If 'match' returns true, the 'mapper' function is invoked to combine the two +// elements into a single result of type 'R'. +// +// Since it evaluates all possible pairs, it supports arbitrary matching criteria +// (e.g., inequalities, regex, or complex logical conditions). +// +// Time Complexity: O(N * M), where N is len(left) and M is len(right). +// Space Complexity: O(1) beyond the memory required for the resulting slice. +func NestJoin[J, K, R any]( + left []J, + right []K, + match func(J, K) bool, + mapper func(J, K) R, +) []R { + var r []R + + for _, j := range left { + for _, k := range right { + if !match(j, k) { + continue + } + r = append(r, mapper(j, k)) + } + } + + return r +} + +// LeftJoin performs a left outer join between two slices based on a common key. +// +// It builds a lookup table (map) from the 'right' slice using 'rk' to extract keys. +// Then, it iterates through the 'left' slice, retrieves the corresponding element +// from the map using the key extracted by 'lk', and projects the result using 'mapper'. +// +// Behavior Notes: +// - If multiple elements in 'right' share the same key, 'KeyBy' will overwrite +// previous values, keeping only the last one. +// - If a key from the 'left' slice does not exist in 'right', the 'mapper' +// is still called, passing the zero value of type 'RE' for the right side. +// - The length of the returned slice is always equal to len(left). +// +// Time Complexity: O(N + M), where N is len(left) and M is len(right), leveraging +// hash-based lookup for optimal performance with large datasets. +// Space Complexity: O(M) to store the intermediate lookup map for the right slice. +func LeftJoin[K comparable, LE, RE, R any]( + left []LE, + right []RE, + lk func(item LE) K, + rk func(item RE) K, + mapper func(LE, RE) R, +) []R { + if len(left) == 0 { + return nil + } + + var r = make([]R, 0, len(left)) + + rm := KeyBy(right, rk) + + for _, j := range left { + k := lk(j) + re := rm[k] + r = append(r, mapper(j, re)) + } + + return r +} diff --git a/tuples_test.go b/tuples_test.go index 659d0ddf..6b4a1e48 100644 --- a/tuples_test.go +++ b/tuples_test.go @@ -2,6 +2,7 @@ package lo import ( "errors" + "fmt" "strconv" "strings" "testing" @@ -1613,3 +1614,306 @@ func TestUnzipByErr(t *testing.T) { is.Equal(2, callbackCount) }) } + +func ExampleNestJoin() { + type User struct { + Id uint64 + Name string + } + + type Book struct { + Id uint64 + Title string + Author uint64 // = User.Id + } + + type BookWithUser struct { + Book + UserName string + } + + user1 := User{ + Id: 1, + Name: "tt", + } + book1 := Book{ + Id: 1, + Title: "Watcher from mikey", + Author: 1, + } + UserBookMatcher := func(j User, k Book) bool { + return j.Id == k.Author + } + + result := NestJoin([]User{user1}, []Book{book1}, UserBookMatcher, func(j User, k Book) BookWithUser { + return BookWithUser{ + Book: k, + UserName: j.Name, + } + }) + fmt.Printf("%v", result) + // Output: [{{1 Watcher from mikey 1} tt}] +} + +func TestNestJoin(t *testing.T) { + t.Parallel() + is := assert.New(t) + + type User struct { + Id uint64 + Name string + } + + type Book struct { + Id uint64 + Title string + Author uint64 // = User.Id + } + + type BookWithUser struct { + Book + UserName string + } + + UserBookMatcher := func(j User, k Book) bool { + return j.Id == k.Author + } + + user1 := User{ + Id: 1, + Name: "tt", + } + user2 := User{ + Id: 2, + Name: "t2", + } + book1 := Book{ + Id: 1, + Title: "Watcher from mikey", + Author: 1, + } + book2 := Book{ + Id: 2, + Title: "Day after day", + Author: 2, + } + type args struct { + users []User + books []Book + } + for _, tt := range []struct { + args args + want []BookWithUser + }{ + { + args: args{ + users: []User{user1}, + books: []Book{book1}, + }, + want: []BookWithUser{ + { + Book: book1, + UserName: user1.Name, + }, + }, + }, + { + args: args{ + users: []User{user2}, + books: []Book{book2}, + }, + want: []BookWithUser{ + { + Book: book2, + UserName: user2.Name, + }, + }, + }, + { + args: args{ + users: []User{user1}, + books: []Book{book2}, + }, + want: nil, + }, + { + args: args{ + users: nil, + books: nil, + }, + want: nil, + }, + } { + r := NestJoin(tt.args.users, tt.args.books, UserBookMatcher, func(j User, k Book) BookWithUser { + return BookWithUser{ + Book: k, + UserName: j.Name, + } + }) + is.Equal(r, tt.want) + } +} + +func ExampleLeftJoin() { + type User struct { + Id uint64 + Name string + } + + type Book struct { + Id uint64 + Title string + Author uint64 // = User.Id + } + + type BookWithUser struct { + Book + UserName string + } + + user1 := User{ + Id: 1, + Name: "tt", + } + user2 := User{ + Id: 2, + Name: "t2", + } + book1 := Book{ + Id: 1, + Title: "Watcher from mikey", + Author: 1, + } + lk := func(item User) uint64 { + return item.Id + } + rk := func(item Book) uint64 { + return item.Author + } + + result := LeftJoin([]User{user1, user2}, []Book{book1}, lk, rk, func(j User, k Book) BookWithUser { + return BookWithUser{ + Book: k, + UserName: j.Name, + } + }) + fmt.Printf("%v", result) + // Output: [{{1 Watcher from mikey 1} tt} {{0 0} t2}] +} + +func TestLeftJoin(t *testing.T) { + t.Parallel() + is := assert.New(t) + + type User struct { + Id uint64 + Name string + } + + type Book struct { + Id uint64 + Title string + Author uint64 // = User.Id + } + + type BookWithUser struct { + Book + UserName string + } + + user1 := User{ + Id: 1, + Name: "tt", + } + user2 := User{ + Id: 2, + Name: "t2", + } + book1 := Book{ + Id: 1, + Title: "Watcher from mikey", + Author: 1, + } + book2 := Book{ + Id: 2, + Title: "Day after day", + Author: 2, + } + + lk := func(item User) uint64 { + return item.Id + } + rk := func(item Book) uint64 { + return item.Author + } + + type args struct { + users []User + books []Book + lk func(item User) uint64 + rk func(item Book) uint64 + } + for _, tt := range []struct { + args args + want []BookWithUser + }{ + { + args: args{ + users: []User{user1}, + books: []Book{book1}, + lk: lk, + rk: rk, + }, + want: []BookWithUser{ + { + Book: book1, + UserName: user1.Name, + }, + }, + }, + { + args: args{ + users: []User{user2}, + books: []Book{book2}, + lk: lk, + rk: rk, + }, + want: []BookWithUser{ + { + Book: book2, + UserName: user2.Name, + }, + }, + }, + { + args: args{ + users: []User{user1}, + books: []Book{book2}, + lk: lk, + rk: rk, + }, + want: []BookWithUser{ + { + UserName: user1.Name, + }, + }, + }, + { + args: args{ + users: nil, + books: nil, + lk: lk, + rk: rk, + }, + want: nil, + }, + } { + r := LeftJoin(tt.args.users, tt.args.books, tt.args.lk, tt.args.rk, func(j User, k Book) BookWithUser { + return BookWithUser{ + Book: k, + UserName: j.Name, + } + }) + is.Equal(r, tt.want) + } +}