From ec014e25a85aad840a01e28cc9f61d706188560c Mon Sep 17 00:00:00 2001 From: Milan Thakor Date: Wed, 23 Oct 2024 21:10:33 +0530 Subject: [PATCH 1/8] Initial commit. Add code to fetch data --- go.mod | 3 +++ main.go | 11 +++++++++++ movie.go | 37 +++++++++++++++++++++++++++++++++++++ person.go | 36 ++++++++++++++++++++++++++++++++++++ utils.go | 31 +++++++++++++++++++++++++++++++ 5 files changed, 118 insertions(+) create mode 100644 go.mod create mode 100644 main.go create mode 100644 movie.go create mode 100644 person.go create mode 100644 utils.go diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..7b3f15b --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/milanvthakor/qube-cinema-task + +go 1.23.1 diff --git a/main.go b/main.go new file mode 100644 index 0000000..8445d83 --- /dev/null +++ b/main.go @@ -0,0 +1,11 @@ +package main + +const baseURL = "https://data.moviebuff.com" + +func degrees(person1 string, person2 string) int { + return -1 +} + +func main() { + +} diff --git a/movie.go b/movie.go new file mode 100644 index 0000000..e5c10bc --- /dev/null +++ b/movie.go @@ -0,0 +1,37 @@ +package main + +import ( + "encoding/json" + "log" +) + +type PersonRole struct { + URL string `json:"url"` + Name string `json:"name"` + Role string `json:"role"` +} + +type Movie struct { + URL string `json:"url"` + Type string `json:"type"` + Name string `json:"name"` + Cast []PersonRole `json:"cast"` + Crew []PersonRole `json:"crew"` +} + +func FetchMovieDetails(movieURL string) (Movie, error) { + data, err := FetchData(movieURL) + if err != nil { + log.Println("Error fetching movie data: ", err) + return Movie{}, err + } + + var movie Movie + err = json.Unmarshal(data, &movie) + if err != nil { + log.Println("Error unmarshalling movie data: ", err) + return Movie{}, err + } + + return movie, nil +} diff --git a/person.go b/person.go new file mode 100644 index 0000000..2e52801 --- /dev/null +++ b/person.go @@ -0,0 +1,36 @@ +package main + +import ( + "encoding/json" + "log" +) + +type MovieRole struct { + Name string `json:"name"` + URL string `json:"url"` + Role string `json:"role"` +} + +type Person struct { + URL string `json:"url"` + Type string `json:"type"` + Name string `json:"name"` + Movies []MovieRole `json:"movies"` +} + +func FetchPersonDetails(personURL string) (Person, error) { + data, err := FetchData(personURL) + if err != nil { + log.Println("Error fetching person data: ", err) + return Person{}, err + } + + var person Person + err = json.Unmarshal(data, &person) + if err != nil { + log.Println("Error unmarshalling person data: ", err) + return Person{}, err + } + + return person, nil +} diff --git a/utils.go b/utils.go new file mode 100644 index 0000000..290931b --- /dev/null +++ b/utils.go @@ -0,0 +1,31 @@ +package main + +import ( + "fmt" + "io" + "log" + "net/http" +) + +func FetchData(moviebuffURL string) ([]byte, error) { + res, err := http.Get(fmt.Sprintf("%s/%s", baseURL, moviebuffURL)) + if err != nil { + log.Println("Error fetching data: ", err) + return nil, err + } + + data, err := io.ReadAll(res.Body) + if err != nil { + log.Println("Error reading data: ", err) + return nil, err + } + + switch res.StatusCode { + case 200: + return data, nil + + default: + log.Println("Invalid status code: ", res.StatusCode) + return nil, fmt.Errorf("movie not found") + } +} From ee66cdc54f91f5d3a983c30188a7145c53983d96 Mon Sep 17 00:00:00 2001 From: Milan Thakor Date: Wed, 23 Oct 2024 22:43:31 +0530 Subject: [PATCH 2/8] Intermediate naive solution with visiting all the branches. --- constants.go | 3 ++ main.go | 92 +++++++++++++++++++++++++++++++++++++++++++++++++--- movie.go | 40 ++++++++++++++++++++++- person.go | 26 ++++++++++++++- utils.go | 8 ++--- visited.go | 30 +++++++++++++++++ 6 files changed, 189 insertions(+), 10 deletions(-) create mode 100644 constants.go create mode 100644 visited.go diff --git a/constants.go b/constants.go new file mode 100644 index 0000000..468ecc6 --- /dev/null +++ b/constants.go @@ -0,0 +1,3 @@ +package main + +const baseURL = "https://data.moviebuff.com" diff --git a/main.go b/main.go index 8445d83..4aecf05 100644 --- a/main.go +++ b/main.go @@ -1,11 +1,95 @@ package main -const baseURL = "https://data.moviebuff.com" +import ( + "fmt" + "math" + "sync" + "sync/atomic" +) -func degrees(person1 string, person2 string) int { - return -1 +var minDegress atomic.Int64 + +func Degrees(person1 string, person2 string, curDegrees int64) (int64, bool) { + if curDegrees >= minDegress.Load() { + return 0, false + } + + person1Data, err := FetchPersonDetails(person1) + if err != nil { + return -1, false + } + + visitedSources.Add(person1) + movies := make([]Movie, 0) + for _, movieRole := range person1Data.Movies { + if visitedSources.Has(movieRole.URL) { + continue + } + + movieData, err := FetchMovieDetails(movieRole.URL) + if err != nil { + continue + } + + visitedSources.Add(movieRole.URL) + movies = append(movies, movieData) + } + + for _, movie := range movies { + for _, person := range movie.Cast { + if person.URL == person2 { + return curDegrees + 1, true + } + } + + for _, person := range movie.Crew { + if person.URL == person2 { + return curDegrees + 1, true + } + } + } + + var wg sync.WaitGroup + for _, movie := range movies { + for _, person := range movie.Cast { + if visitedSources.Has(person.URL) { + continue + } + + wg.Add(1) + go func() { + defer wg.Done() + + degrees, ok := Degrees(person.URL, person2, curDegrees+1) + if ok && degrees < minDegress.Load() { + minDegress.Store(degrees) + } + }() + } + + for _, person := range movie.Crew { + if visitedSources.Has(person.URL) { + continue + } + + wg.Add(1) + go func() { + defer wg.Done() + + degrees, ok := Degrees(person.URL, person2, curDegrees+1) + if ok && degrees < minDegress.Load() { + minDegress.Store(degrees) + } + }() + } + } + + wg.Wait() + return 0, false } func main() { - + minDegress.Store(math.MaxInt64) + Degrees("amitabh-bachchan", "robert-de-niro", 0) + fmt.Println(minDegress.Load()) } diff --git a/movie.go b/movie.go index e5c10bc..c418d71 100644 --- a/movie.go +++ b/movie.go @@ -2,6 +2,7 @@ package main import ( "encoding/json" + "fmt" "log" ) @@ -22,7 +23,6 @@ type Movie struct { func FetchMovieDetails(movieURL string) (Movie, error) { data, err := FetchData(movieURL) if err != nil { - log.Println("Error fetching movie data: ", err) return Movie{}, err } @@ -35,3 +35,41 @@ func FetchMovieDetails(movieURL string) (Movie, error) { return movie, nil } + +func MoviesPeople(movieRoles []MovieRole) map[string]PersonRole { + moviePeople := make(map[string]PersonRole) + for _, movieRole := range movieRoles { + movieData, err := FetchMovieDetails(movieRole.URL) + if err != nil { + continue + } + + for _, person := range movieData.Cast { + moviePeople[person.URL] = person + } + + for _, person := range movieData.Crew { + moviePeople[person.URL] = person + } + } + + return moviePeople +} + +func HasSamePerson(moviesPeople1 map[string]PersonRole, moviesPeople2 map[string]PersonRole) bool { + for personURL := range moviesPeople1 { + if _, ok := moviesPeople2[personURL]; ok { + fmt.Println(moviesPeople1[personURL].URL) + return true + } + } + + for personURL := range moviesPeople2 { + if _, ok := moviesPeople1[personURL]; ok { + fmt.Println(moviesPeople1[personURL].URL) + return true + } + } + + return false +} diff --git a/person.go b/person.go index 2e52801..596cfad 100644 --- a/person.go +++ b/person.go @@ -21,7 +21,6 @@ type Person struct { func FetchPersonDetails(personURL string) (Person, error) { data, err := FetchData(personURL) if err != nil { - log.Println("Error fetching person data: ", err) return Person{}, err } @@ -34,3 +33,28 @@ func FetchPersonDetails(personURL string) (Person, error) { return person, nil } + +func ToMovieRoleSet(movieRoles []MovieRole) map[string]MovieRole { + movieRoleSet := make(map[string]MovieRole) + for _, movieRole := range movieRoles { + movieRoleSet[movieRole.URL] = movieRole + } + + return movieRoleSet +} + +func HasSameMovie(movieRoleSet1 map[string]MovieRole, movieRoleSet2 map[string]MovieRole) bool { + for movieURL := range movieRoleSet1 { + if _, ok := movieRoleSet2[movieURL]; ok { + return true + } + } + + for movieURL := range movieRoleSet2 { + if _, ok := movieRoleSet1[movieURL]; ok { + return true + } + } + + return false +} diff --git a/utils.go b/utils.go index 290931b..a2dd04b 100644 --- a/utils.go +++ b/utils.go @@ -10,13 +10,13 @@ import ( func FetchData(moviebuffURL string) ([]byte, error) { res, err := http.Get(fmt.Sprintf("%s/%s", baseURL, moviebuffURL)) if err != nil { - log.Println("Error fetching data: ", err) + log.Println("Error fetching data.", "URL", moviebuffURL, "Error", err) return nil, err } data, err := io.ReadAll(res.Body) if err != nil { - log.Println("Error reading data: ", err) + log.Println("Error reading data.", "URL", moviebuffURL, "Error", err) return nil, err } @@ -25,7 +25,7 @@ func FetchData(moviebuffURL string) ([]byte, error) { return data, nil default: - log.Println("Invalid status code: ", res.StatusCode) - return nil, fmt.Errorf("movie not found") + log.Println("Invalid response.", "URL", moviebuffURL, "Status code", res.StatusCode, "Data", string(data)) + return nil, fmt.Errorf("invalid response. Status code: %d. Data: %s", res.StatusCode, string(data)) } } diff --git a/visited.go b/visited.go new file mode 100644 index 0000000..e9fab54 --- /dev/null +++ b/visited.go @@ -0,0 +1,30 @@ +package main + +import "sync" + +type VisitedSources struct { + URLs map[string]struct{} + mutex *sync.RWMutex +} + +func NewVisitedSources() VisitedSources { + return VisitedSources{ + URLs: make(map[string]struct{}), + mutex: &sync.RWMutex{}, + } +} + +func (vs *VisitedSources) Add(url string) { + vs.mutex.Lock() + defer vs.mutex.Unlock() + vs.URLs[url] = struct{}{} +} + +func (vs *VisitedSources) Has(url string) bool { + vs.mutex.RLock() + defer vs.mutex.RUnlock() + _, ok := vs.URLs[url] + return ok +} + +var visitedSources = NewVisitedSources() From c60df0581c31ce6564ebfd054dd54a002b54c0bb Mon Sep 17 00:00:00 2001 From: Milan Thakor Date: Thu, 24 Oct 2024 10:34:55 +0530 Subject: [PATCH 3/8] Initial implementations of BFS traversal with parallelism. --- main.go | 135 ++++++++++++++++++++++++++---------------------------- result.go | 24 ++++++++++ worker.go | 59 ++++++++++++++++++++++++ 3 files changed, 147 insertions(+), 71 deletions(-) create mode 100644 result.go create mode 100644 worker.go diff --git a/main.go b/main.go index 4aecf05..f0990ea 100644 --- a/main.go +++ b/main.go @@ -1,95 +1,88 @@ package main import ( - "fmt" - "math" "sync" - "sync/atomic" ) -var minDegress atomic.Int64 - -func Degrees(person1 string, person2 string, curDegrees int64) (int64, bool) { - if curDegrees >= minDegress.Load() { - return 0, false - } - - person1Data, err := FetchPersonDetails(person1) - if err != nil { - return -1, false +func Degrees(person1 string, person2 string) { + jobQu := []Job{ + { + person: person1, + degree: 0, + connections: []Connection{}, + }, } - visitedSources.Add(person1) - movies := make([]Movie, 0) - for _, movieRole := range person1Data.Movies { - if visitedSources.Has(movieRole.URL) { - continue - } - movieData, err := FetchMovieDetails(movieRole.URL) - if err != nil { - continue - } - - visitedSources.Add(movieRole.URL) - movies = append(movies, movieData) - } - - for _, movie := range movies { - for _, person := range movie.Cast { - if person.URL == person2 { - return curDegrees + 1, true + for len(jobQu) > 0 { + qusize := len(jobQu) + jobs := make(chan Job, qusize) + results := make(chan Job) + poolGroup := &sync.WaitGroup{} + poolResult := make(chan struct { + jobQu []Job + done bool + }, 1) + + poolGroup.Add(1) + go func() { + defer poolGroup.Done() + + workerGroup := &sync.WaitGroup{} + for i := 0; i < 100; i++ { + workerGroup.Add(1) + go Worker(workerGroup, person2, jobs, results) } - } - for _, person := range movie.Crew { - if person.URL == person2 { - return curDegrees + 1, true + for i := 0; i < qusize; i++ { + jobs <- jobQu[i] } - } - } + close(jobs) + + workerGroup.Wait() + close(results) + }() + + poolGroup.Add(1) + go func() { + defer poolGroup.Done() + + newJobQ := []Job{} + for result := range results { + if result.done { + PrintResult(result.connections, result.degree) + poolResult <- struct { + jobQu []Job + done bool + }{ + done: true, + } + return + } - var wg sync.WaitGroup - for _, movie := range movies { - for _, person := range movie.Cast { - if visitedSources.Has(person.URL) { - continue + newJobQ = append(newJobQ, result) } - wg.Add(1) - go func() { - defer wg.Done() - - degrees, ok := Degrees(person.URL, person2, curDegrees+1) - if ok && degrees < minDegress.Load() { - minDegress.Store(degrees) - } - }() - } - - for _, person := range movie.Crew { - if visitedSources.Has(person.URL) { - continue + poolResult <- struct { + jobQu []Job + done bool + }{ + jobQu: newJobQ, } + }() - wg.Add(1) - go func() { - defer wg.Done() + poolGroup.Wait() + close(poolResult) - degrees, ok := Degrees(person.URL, person2, curDegrees+1) - if ok && degrees < minDegress.Load() { - minDegress.Store(degrees) - } - }() + res := <-poolResult + if res.done { + return } - } - wg.Wait() - return 0, false + jobQu = res.jobQu + } } func main() { - minDegress.Store(math.MaxInt64) - Degrees("amitabh-bachchan", "robert-de-niro", 0) - fmt.Println(minDegress.Load()) + Degrees("amitabh-bachchan", "robert-de-niro") } diff --git a/result.go b/result.go new file mode 100644 index 0000000..2cb8460 --- /dev/null +++ b/result.go @@ -0,0 +1,24 @@ +package main + +import ( + "fmt" +) + +type Connection struct { + Movie string `json:"movie"` + Person1 string `json:"person1"` + Person1Role string `json:"person1_role"` + Person2 string `json:"person2"` + Person2Role string `json:"person2_role"` +} + +func PrintResult(result []Connection, degree int64) { + fmt.Println("Degree of Seperation: ", degree) + + for i, connection := range result { + fmt.Printf("%d. Movie: %s\n", i+1, connection.Movie) + fmt.Printf("%s: %s\n", connection.Person1Role, connection.Person1) + fmt.Printf("%s: %s\n", connection.Person2Role, connection.Person2) + fmt.Println() + } +} diff --git a/worker.go b/worker.go new file mode 100644 index 0000000..10fd788 --- /dev/null +++ b/worker.go @@ -0,0 +1,59 @@ +package main + +import "sync" + +type Job struct { + person string + degree int64 + connections []Connection + done bool +} + +func Worker(wg *sync.WaitGroup, person2 string, jobs <-chan Job, results chan<- Job) { + defer wg.Done() + + for j := range jobs { + personData, err := FetchPersonDetails(j.person) + if err != nil { + return + } + + for _, movieRole := range personData.Movies { + movieData, err := FetchMovieDetails(movieRole.URL) + if err != nil { + continue + } + + people := append(movieData.Cast, movieData.Crew...) + + for _, person := range people { + if visitedSources.Has(person.URL) { + continue + } + + connections := append(j.connections, Connection{ + Movie: movieData.Name, + Person1: personData.Name, + Person1Role: movieRole.Role, + Person2: person.Name, + Person2Role: person.Role, + }) + + newJob := Job{ + person: person.URL, + degree: j.degree + 1, + connections: connections, + } + + if person.URL == person2 { + newJob.done = true + results <- newJob + return + } + + visitedSources.Add(person.URL) + results <- newJob + } + } + } +} From be268563d697c2b2a265c7f716c165c0fc46723d Mon Sep 17 00:00:00 2001 From: Milan Thakor Date: Thu, 24 Oct 2024 19:38:02 +0530 Subject: [PATCH 4/8] Refactor the code to fetch the API. --- api.go | 101 +++++++++++++++++++++++++++++++++++++++++++++++++++ constants.go | 3 -- movie.go | 75 -------------------------------------- person.go | 60 ------------------------------ utils.go | 31 ---------------- worker.go | 4 +- 6 files changed, 103 insertions(+), 171 deletions(-) create mode 100644 api.go delete mode 100644 constants.go delete mode 100644 movie.go delete mode 100644 person.go delete mode 100644 utils.go diff --git a/api.go b/api.go new file mode 100644 index 0000000..a0834f5 --- /dev/null +++ b/api.go @@ -0,0 +1,101 @@ +package main + +import ( + "encoding/json" + "fmt" + "io" + "log/slog" + "net/http" +) + +// CommonData is the common data that exists in both Person and Movie. +type CommonData struct { + URL string `json:"url"` + Type string `json:"type"` + Name string `json:"name"` +} + +// Participation is the data that represents both: +// 1. A movie in which a person has played a role. +// 2. A person who played a role in a movie. +type Participation struct { + URL string `json:"url"` + Name string `json:"name"` + Role string `json:"role"` +} + +// Person is the data that represents a person. +type Person struct { + CommonData + Movies []Participation `json:"movies"` +} + +// Movie is the data that represents a movie. +type Movie struct { + CommonData + Cast []Participation `json:"cast"` + Crew []Participation `json:"crew"` +} + +// baseURL is the base URL for the Moviebuff API. +const baseURL = "https://data.moviebuff.com" + +// FetchData is a helper function that fetches data from any Moviebuff URL. +func FetchData(moviebuffURL string) ([]byte, error) { + logger := slog.With("URL", moviebuffURL) + + res, err := http.Get(fmt.Sprintf("%s/%s", baseURL, moviebuffURL)) + if err != nil { + logger.Error("Error fetching data.", "Error", err) + return nil, err + } + + data, err := io.ReadAll(res.Body) + if err != nil { + logger.Error("Error reading data.", "Error", err) + return nil, err + } + + switch res.StatusCode { + case 200: + return data, nil + + default: + logger.Error("Invalid response.", "Status code", res.StatusCode, "Data", string(data)) + return nil, fmt.Errorf("invalid response. Status code: %d. Data: %s", res.StatusCode, string(data)) + } +} + +// FetchMovie is a helper function that fetches movie data from a Moviebuff URL. +func FetchMovie(movieURL string) (Movie, error) { + data, err := FetchData(movieURL) + if err != nil { + return Movie{}, err + } + + var movie Movie + err = json.Unmarshal(data, &movie) + if err != nil { + slog.Error("Error unmarshalling movie data.", "Error", err) + return Movie{}, err + } + + return movie, nil +} + +// FetchPerson is a helper function that fetches person data from a Moviebuff URL. +func FetchPerson(personURL string) (Person, error) { + data, err := FetchData(personURL) + if err != nil { + return Person{}, err + } + + var person Person + err = json.Unmarshal(data, &person) + if err != nil { + slog.Error("Error unmarshalling person data.", "Error", err) + return Person{}, err + } + + return person, nil +} diff --git a/constants.go b/constants.go deleted file mode 100644 index 468ecc6..0000000 --- a/constants.go +++ /dev/null @@ -1,3 +0,0 @@ -package main - -const baseURL = "https://data.moviebuff.com" diff --git a/movie.go b/movie.go deleted file mode 100644 index c418d71..0000000 --- a/movie.go +++ /dev/null @@ -1,75 +0,0 @@ -package main - -import ( - "encoding/json" - "fmt" - "log" -) - -type PersonRole struct { - URL string `json:"url"` - Name string `json:"name"` - Role string `json:"role"` -} - -type Movie struct { - URL string `json:"url"` - Type string `json:"type"` - Name string `json:"name"` - Cast []PersonRole `json:"cast"` - Crew []PersonRole `json:"crew"` -} - -func FetchMovieDetails(movieURL string) (Movie, error) { - data, err := FetchData(movieURL) - if err != nil { - return Movie{}, err - } - - var movie Movie - err = json.Unmarshal(data, &movie) - if err != nil { - log.Println("Error unmarshalling movie data: ", err) - return Movie{}, err - } - - return movie, nil -} - -func MoviesPeople(movieRoles []MovieRole) map[string]PersonRole { - moviePeople := make(map[string]PersonRole) - for _, movieRole := range movieRoles { - movieData, err := FetchMovieDetails(movieRole.URL) - if err != nil { - continue - } - - for _, person := range movieData.Cast { - moviePeople[person.URL] = person - } - - for _, person := range movieData.Crew { - moviePeople[person.URL] = person - } - } - - return moviePeople -} - -func HasSamePerson(moviesPeople1 map[string]PersonRole, moviesPeople2 map[string]PersonRole) bool { - for personURL := range moviesPeople1 { - if _, ok := moviesPeople2[personURL]; ok { - fmt.Println(moviesPeople1[personURL].URL) - return true - } - } - - for personURL := range moviesPeople2 { - if _, ok := moviesPeople1[personURL]; ok { - fmt.Println(moviesPeople1[personURL].URL) - return true - } - } - - return false -} diff --git a/person.go b/person.go deleted file mode 100644 index 596cfad..0000000 --- a/person.go +++ /dev/null @@ -1,60 +0,0 @@ -package main - -import ( - "encoding/json" - "log" -) - -type MovieRole struct { - Name string `json:"name"` - URL string `json:"url"` - Role string `json:"role"` -} - -type Person struct { - URL string `json:"url"` - Type string `json:"type"` - Name string `json:"name"` - Movies []MovieRole `json:"movies"` -} - -func FetchPersonDetails(personURL string) (Person, error) { - data, err := FetchData(personURL) - if err != nil { - return Person{}, err - } - - var person Person - err = json.Unmarshal(data, &person) - if err != nil { - log.Println("Error unmarshalling person data: ", err) - return Person{}, err - } - - return person, nil -} - -func ToMovieRoleSet(movieRoles []MovieRole) map[string]MovieRole { - movieRoleSet := make(map[string]MovieRole) - for _, movieRole := range movieRoles { - movieRoleSet[movieRole.URL] = movieRole - } - - return movieRoleSet -} - -func HasSameMovie(movieRoleSet1 map[string]MovieRole, movieRoleSet2 map[string]MovieRole) bool { - for movieURL := range movieRoleSet1 { - if _, ok := movieRoleSet2[movieURL]; ok { - return true - } - } - - for movieURL := range movieRoleSet2 { - if _, ok := movieRoleSet1[movieURL]; ok { - return true - } - } - - return false -} diff --git a/utils.go b/utils.go deleted file mode 100644 index a2dd04b..0000000 --- a/utils.go +++ /dev/null @@ -1,31 +0,0 @@ -package main - -import ( - "fmt" - "io" - "log" - "net/http" -) - -func FetchData(moviebuffURL string) ([]byte, error) { - res, err := http.Get(fmt.Sprintf("%s/%s", baseURL, moviebuffURL)) - if err != nil { - log.Println("Error fetching data.", "URL", moviebuffURL, "Error", err) - return nil, err - } - - data, err := io.ReadAll(res.Body) - if err != nil { - log.Println("Error reading data.", "URL", moviebuffURL, "Error", err) - return nil, err - } - - switch res.StatusCode { - case 200: - return data, nil - - default: - log.Println("Invalid response.", "URL", moviebuffURL, "Status code", res.StatusCode, "Data", string(data)) - return nil, fmt.Errorf("invalid response. Status code: %d. Data: %s", res.StatusCode, string(data)) - } -} diff --git a/worker.go b/worker.go index 10fd788..dc9272d 100644 --- a/worker.go +++ b/worker.go @@ -13,13 +13,13 @@ func Worker(wg *sync.WaitGroup, person2 string, jobs <-chan Job, results chan<- defer wg.Done() for j := range jobs { - personData, err := FetchPersonDetails(j.person) + personData, err := FetchPerson(j.person) if err != nil { return } for _, movieRole := range personData.Movies { - movieData, err := FetchMovieDetails(movieRole.URL) + movieData, err := FetchMovie(movieRole.URL) if err != nil { continue } From 12d7c36c21686fbd012c97df1b3b3f02047f35a6 Mon Sep 17 00:00:00 2001 From: Milan Thakor Date: Thu, 24 Oct 2024 19:47:33 +0530 Subject: [PATCH 5/8] Rename visited-source and collaboration struct. --- main.go | 6 ++++-- result.go | 24 ------------------------ utils.go | 30 ++++++++++++++++++++++++++++++ visited.go => visited-sources.go | 7 +++++-- worker.go | 4 ++-- 5 files changed, 41 insertions(+), 30 deletions(-) delete mode 100644 result.go create mode 100644 utils.go rename visited.go => visited-sources.go (59%) diff --git a/main.go b/main.go index f0990ea..567c3bd 100644 --- a/main.go +++ b/main.go @@ -4,12 +4,14 @@ import ( "sync" ) +var visitedSources = NewVisitedSources() + func Degrees(person1 string, person2 string) { jobQu := []Job{ { person: person1, degree: 0, - connections: []Connection{}, + connections: []Collaboration{}, }, } visitedSources.Add(person1) @@ -50,7 +52,7 @@ func Degrees(person1 string, person2 string) { newJobQ := []Job{} for result := range results { if result.done { - PrintResult(result.connections, result.degree) + PrintCollaborations(result.connections, result.degree) poolResult <- struct { jobQu []Job done bool diff --git a/result.go b/result.go deleted file mode 100644 index 2cb8460..0000000 --- a/result.go +++ /dev/null @@ -1,24 +0,0 @@ -package main - -import ( - "fmt" -) - -type Connection struct { - Movie string `json:"movie"` - Person1 string `json:"person1"` - Person1Role string `json:"person1_role"` - Person2 string `json:"person2"` - Person2Role string `json:"person2_role"` -} - -func PrintResult(result []Connection, degree int64) { - fmt.Println("Degree of Seperation: ", degree) - - for i, connection := range result { - fmt.Printf("%d. Movie: %s\n", i+1, connection.Movie) - fmt.Printf("%s: %s\n", connection.Person1Role, connection.Person1) - fmt.Printf("%s: %s\n", connection.Person2Role, connection.Person2) - fmt.Println() - } -} diff --git a/utils.go b/utils.go new file mode 100644 index 0000000..9ebae6e --- /dev/null +++ b/utils.go @@ -0,0 +1,30 @@ +package main + +import ( + "fmt" +) + +// Collaboration represents a collaboration between two people in a movie. +type Collaboration struct { + Movie string + Person1 string + Person1Role string + Person2 string + Person2Role string +} + +// PrintCollaborations prints a list of collaborations. +func PrintCollaborations(collabs []Collaboration, degree int64) { + if len(collabs) == 0 { + fmt.Println("No collaborations found.") + return + } + + fmt.Println("Degree of Seperation: ", degree) + for i, connection := range collabs { + fmt.Printf("%d. Movie: %s\n", i+1, connection.Movie) + fmt.Printf("%s: %s\n", connection.Person1Role, connection.Person1) + fmt.Printf("%s: %s\n", connection.Person2Role, connection.Person2) + fmt.Println() + } +} diff --git a/visited.go b/visited-sources.go similarity index 59% rename from visited.go rename to visited-sources.go index e9fab54..fd92562 100644 --- a/visited.go +++ b/visited-sources.go @@ -2,11 +2,14 @@ package main import "sync" +// VisitedSources is a set of visited sources with a mutex lock for concurrent access. +// This set is used to avoid repeatedly fetching data from the same source and processing it multiple times. type VisitedSources struct { URLs map[string]struct{} mutex *sync.RWMutex } +// NewVisitedSources creates a new VisitedSources. func NewVisitedSources() VisitedSources { return VisitedSources{ URLs: make(map[string]struct{}), @@ -14,17 +17,17 @@ func NewVisitedSources() VisitedSources { } } +// Add adds a URL to the VisitedSources. func (vs *VisitedSources) Add(url string) { vs.mutex.Lock() defer vs.mutex.Unlock() vs.URLs[url] = struct{}{} } +// Has checks if a URL is in the VisitedSources. func (vs *VisitedSources) Has(url string) bool { vs.mutex.RLock() defer vs.mutex.RUnlock() _, ok := vs.URLs[url] return ok } - -var visitedSources = NewVisitedSources() diff --git a/worker.go b/worker.go index dc9272d..8ccc5d8 100644 --- a/worker.go +++ b/worker.go @@ -5,7 +5,7 @@ import "sync" type Job struct { person string degree int64 - connections []Connection + connections []Collaboration done bool } @@ -31,7 +31,7 @@ func Worker(wg *sync.WaitGroup, person2 string, jobs <-chan Job, results chan<- continue } - connections := append(j.connections, Connection{ + connections := append(j.connections, Collaboration{ Movie: movieData.Name, Person1: personData.Name, Person1Role: movieRole.Role, From 1a9d937526cfdd27684f788286336787bdde6de3 Mon Sep 17 00:00:00 2001 From: Milan Thakor Date: Thu, 24 Oct 2024 21:49:33 +0530 Subject: [PATCH 6/8] Complete implementation of BFS with go-routines. --- api.go | 8 -- main.go | 87 +-------------------- visited-sources.go | 4 +- worker.go | 184 +++++++++++++++++++++++++++++++++++++-------- 4 files changed, 156 insertions(+), 127 deletions(-) diff --git a/api.go b/api.go index a0834f5..a796cb7 100644 --- a/api.go +++ b/api.go @@ -4,7 +4,6 @@ import ( "encoding/json" "fmt" "io" - "log/slog" "net/http" ) @@ -42,17 +41,13 @@ const baseURL = "https://data.moviebuff.com" // FetchData is a helper function that fetches data from any Moviebuff URL. func FetchData(moviebuffURL string) ([]byte, error) { - logger := slog.With("URL", moviebuffURL) - res, err := http.Get(fmt.Sprintf("%s/%s", baseURL, moviebuffURL)) if err != nil { - logger.Error("Error fetching data.", "Error", err) return nil, err } data, err := io.ReadAll(res.Body) if err != nil { - logger.Error("Error reading data.", "Error", err) return nil, err } @@ -61,7 +56,6 @@ func FetchData(moviebuffURL string) ([]byte, error) { return data, nil default: - logger.Error("Invalid response.", "Status code", res.StatusCode, "Data", string(data)) return nil, fmt.Errorf("invalid response. Status code: %d. Data: %s", res.StatusCode, string(data)) } } @@ -76,7 +70,6 @@ func FetchMovie(movieURL string) (Movie, error) { var movie Movie err = json.Unmarshal(data, &movie) if err != nil { - slog.Error("Error unmarshalling movie data.", "Error", err) return Movie{}, err } @@ -93,7 +86,6 @@ func FetchPerson(personURL string) (Person, error) { var person Person err = json.Unmarshal(data, &person) if err != nil { - slog.Error("Error unmarshalling person data.", "Error", err) return Person{}, err } diff --git a/main.go b/main.go index 567c3bd..689f89f 100644 --- a/main.go +++ b/main.go @@ -1,90 +1,5 @@ package main -import ( - "sync" -) - -var visitedSources = NewVisitedSources() - -func Degrees(person1 string, person2 string) { - jobQu := []Job{ - { - person: person1, - degree: 0, - connections: []Collaboration{}, - }, - } - visitedSources.Add(person1) - - for len(jobQu) > 0 { - qusize := len(jobQu) - jobs := make(chan Job, qusize) - results := make(chan Job) - poolGroup := &sync.WaitGroup{} - poolResult := make(chan struct { - jobQu []Job - done bool - }, 1) - - poolGroup.Add(1) - go func() { - defer poolGroup.Done() - - workerGroup := &sync.WaitGroup{} - for i := 0; i < 100; i++ { - workerGroup.Add(1) - go Worker(workerGroup, person2, jobs, results) - } - - for i := 0; i < qusize; i++ { - jobs <- jobQu[i] - } - close(jobs) - - workerGroup.Wait() - close(results) - }() - - poolGroup.Add(1) - go func() { - defer poolGroup.Done() - - newJobQ := []Job{} - for result := range results { - if result.done { - PrintCollaborations(result.connections, result.degree) - poolResult <- struct { - jobQu []Job - done bool - }{ - done: true, - } - return - } - - newJobQ = append(newJobQ, result) - } - - poolResult <- struct { - jobQu []Job - done bool - }{ - jobQu: newJobQ, - } - }() - - poolGroup.Wait() - close(poolResult) - - res := <-poolResult - if res.done { - return - } - - jobQu = res.jobQu - } -} - func main() { - Degrees("amitabh-bachchan", "robert-de-niro") + SeperationDegrees("amitabh-bachchan", "milan-thakor") } diff --git a/visited-sources.go b/visited-sources.go index fd92562..b4739a3 100644 --- a/visited-sources.go +++ b/visited-sources.go @@ -10,8 +10,8 @@ type VisitedSources struct { } // NewVisitedSources creates a new VisitedSources. -func NewVisitedSources() VisitedSources { - return VisitedSources{ +func NewVisitedSources() *VisitedSources { + return &VisitedSources{ URLs: make(map[string]struct{}), mutex: &sync.RWMutex{}, } diff --git a/worker.go b/worker.go index 8ccc5d8..c2b8537 100644 --- a/worker.go +++ b/worker.go @@ -1,59 +1,181 @@ package main -import "sync" +import ( + "context" + "sync" +) -type Job struct { - person string - degree int64 - connections []Collaboration - done bool +// GraphSource represents a node in the graph to search for connections. +type GraphSource struct { + // PersonURL is the URL of the person in the Moviebuff API. + PersonURL string + // Degree is the number of connections between the source person and current person. + Degree int64 + // Connections are the details of the connections between the source person and current person. + Connections []Collaboration } -func Worker(wg *sync.WaitGroup, person2 string, jobs <-chan Job, results chan<- Job) { - defer wg.Done() - - for j := range jobs { - personData, err := FetchPerson(j.person) +// GraphTraversalWorker traverses the nodes of the level provided in the "jobs" channel and writes the nodes of the next level to the "results" channel. +func GraphTraversalWorker(ctx context.Context, ctxCancel context.CancelFunc, destPerson string, vs *VisitedSources, jobs <-chan GraphSource, results chan<- GraphSource) { + for job := range jobs { + // Fetch the person data. + personData, err := FetchPerson(job.PersonURL) if err != nil { - return + continue } - for _, movieRole := range personData.Movies { - movieData, err := FetchMovie(movieRole.URL) - if err != nil { + // Iterate over the movies of the person. + for _, movie := range personData.Movies { + // Skip if the movie has already been visited. + if vs.Has(movie.URL) { continue } - people := append(movieData.Cast, movieData.Crew...) + vs.Add(movie.URL) + // Fetch the movie data. + movieData, err := FetchMovie(movie.URL) + if err != nil { + continue + } - for _, person := range people { - if visitedSources.Has(person.URL) { + // movieMembers is the list of cast and crew of the movie. + movieMembers := append(movieData.Cast, movieData.Crew...) + // Iterate over the members of the movie. + for _, member := range movieMembers { + // Skip if the member has already been visited. + if vs.Has(member.URL) { continue } - connections := append(j.connections, Collaboration{ + // Store the details of the collaboration between the source person and the member. + collab := append(job.Connections, Collaboration{ Movie: movieData.Name, Person1: personData.Name, - Person1Role: movieRole.Role, - Person2: person.Name, - Person2Role: person.Role, + Person1Role: movie.Role, + Person2: member.Name, + Person2Role: member.Role, }) - newJob := Job{ - person: person.URL, - degree: j.degree + 1, - connections: connections, + gs := GraphSource{ + PersonURL: member.URL, + Degree: job.Degree + 1, + Connections: collab, + } + + // Check if the destination person has been found. + // If so, print the connections, cancel the context to stop the workers, and return. + if member.URL == destPerson { + PrintCollaborations(gs.Connections, gs.Degree) + ctxCancel() + return + } + + // Add the member to the visited list and write the node to the results channel. + vs.Add(member.URL) + select { + case results <- gs: + case <-ctx.Done(): + return } + } + } + } +} - if person.URL == person2 { - newJob.done = true - results <- newJob +// SeperationDegrees calculates the minimum degrees of seperation i.e. the number of connections between two people. +// This is classical graph theory problem where we need to find the shortest path between two people. +// Each node in the graph represents a person and each edge represents a connection between two people which is a movie. +// However, the tricky part is we only know source and destination people, the rest of the people are hidden. The rest of +// the people will be explored at runtime. +// The BFS algorithm the most efficient in this case as we need to find the shortest path and the rest of the nodes are hidden. +// So, as we do in BFS, we will traverse the graph level by level and the moment we find the destination, we will return the +// number of connections. +func SeperationDegrees(srcPerson string, destPerson string) { + // vs will store both visited people and movies. + vs := NewVisitedSources() + queue := []GraphSource{} + + // Add the source person to the queue. + vs.Add(srcPerson) + queue = append(queue, GraphSource{ + PersonURL: srcPerson, + Degree: 0, + Connections: []Collaboration{}, + }) + + // Traverse the graph level by level. + for len(queue) > 0 { + // ctx will be used to stop processing further when the destination is found. + ctx, ctxCancel := context.WithCancel(context.Background()) + + // jobs is the queue of people to be explored. + jobs := make(chan GraphSource, len(queue)) + // results is the next level of people to be explored. + results := make(chan GraphSource) + + // rwGroup is used to wait for the reader (reading from "results" channel) and writer (writing to "results" channel) to finish. + rwGroup := &sync.WaitGroup{} + // rwGroupChn is used to send the next level of people to be explored from the reader. + rwGroupChn := make(chan []GraphSource, 1) + + // As the we don't know how many people are in the next level, the size of the + // "results" channel is set to 1. That means we need to haver reader always reading from the channel. + // Hence, we need to add a goroutine to read from the channel. + rwGroup.Add(1) + go func() { + defer rwGroup.Done() + + newQueue := []GraphSource{} + for { + select { + case result, ok := <-results: + if !ok { + rwGroupChn <- newQueue + return + } + newQueue = append(newQueue, result) + + case <-ctx.Done(): return } + } + }() - visitedSources.Add(person.URL) - results <- newJob + // Add a goroutine to process the people in the queue. + rwGroup.Add(1) + go func() { + defer rwGroup.Done() + + // As the number of people to be explored could be huge, we need to process them in batches. + wg := &sync.WaitGroup{} + for i := 0; i < 100; i++ { + wg.Add(1) + go func() { + defer wg.Done() + GraphTraversalWorker(ctx, ctxCancel, destPerson, vs, jobs, results) + }() + } + + // Add the people in the queue to the jobs channel. + for _, gs := range queue { + jobs <- gs } + close(jobs) + + // Wait for the workers to finish. Then close the results channel so that the reader can finish. + wg.Wait() + close(results) + }() + + rwGroup.Wait() + close(rwGroupChn) + + select { + case <-ctx.Done(): + return + case queue = <-rwGroupChn: } } + + PrintCollaborations(nil, 0) } From ea8778a018accb4c81c51b029c4f69bbe357453c Mon Sep 17 00:00:00 2001 From: Milan Thakor Date: Thu, 24 Oct 2024 22:37:55 +0530 Subject: [PATCH 7/8] Fix issue with multiple output print and add spinner. --- go.mod | 10 ++++++++++ go.sum | 13 +++++++++++++ main.go | 28 +++++++++++++++++++++++++++- utils.go | 4 ++-- worker.go | 53 ++++++++++++++++++++++++++++++++--------------------- 5 files changed, 84 insertions(+), 24 deletions(-) create mode 100644 go.sum diff --git a/go.mod b/go.mod index 7b3f15b..d12d0fc 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,13 @@ module github.com/milanvthakor/qube-cinema-task go 1.23.1 + +require github.com/briandowns/spinner v1.23.1 + +require ( + github.com/fatih/color v1.7.0 // indirect + github.com/mattn/go-colorable v0.1.2 // indirect + github.com/mattn/go-isatty v0.0.8 // indirect + golang.org/x/sys v0.0.0-20220412211240-33da011f77ad // indirect + golang.org/x/term v0.1.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..9cad878 --- /dev/null +++ b/go.sum @@ -0,0 +1,13 @@ +github.com/briandowns/spinner v1.23.1 h1:t5fDPmScwUjozhDj4FA46p5acZWIPXYE30qW2Ptu650= +github.com/briandowns/spinner v1.23.1/go.mod h1:LaZeM4wm2Ywy6vO571mvhQNRcWfRUnXOs0RcKV0wYKM= +github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU= +github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad h1:ntjMns5wyP/fN65tdBD4g8J5w8n015+iIIs9rtjXkY0= +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.1.0 h1:g6Z6vPFA9dYBAF7DWcH6sCcOntplXsDKcliusYijMlw= +golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= diff --git a/main.go b/main.go index 689f89f..1d46d4e 100644 --- a/main.go +++ b/main.go @@ -1,5 +1,31 @@ package main +import ( + "fmt" + "os" + "time" + + "github.com/briandowns/spinner" +) + func main() { - SeperationDegrees("amitabh-bachchan", "milan-thakor") + if len(os.Args) < 3 { + fmt.Println("Error: Invalid number of arguments.") + fmt.Println("Usage: go run . ") + return + } + + moviebuffURL1 := os.Args[1] + moviebuffURL2 := os.Args[2] + if moviebuffURL1 == moviebuffURL2 { + fmt.Println("Error: Moviebuff URLs must be different.") + return + } + + fmt.Println("Searching Moviebuff...") + s := spinner.New(spinner.CharSets[9], 100*time.Millisecond) + s.Start() + defer s.Stop() + + SeperationDegrees(moviebuffURL1, moviebuffURL2) } diff --git a/utils.go b/utils.go index 9ebae6e..5e7b4cf 100644 --- a/utils.go +++ b/utils.go @@ -16,11 +16,11 @@ type Collaboration struct { // PrintCollaborations prints a list of collaborations. func PrintCollaborations(collabs []Collaboration, degree int64) { if len(collabs) == 0 { - fmt.Println("No collaborations found.") + fmt.Println("\nNo collaborations found.") return } - fmt.Println("Degree of Seperation: ", degree) + fmt.Println("\nDegree of Seperation: ", degree) for i, connection := range collabs { fmt.Printf("%d. Movie: %s\n", i+1, connection.Movie) fmt.Printf("%s: %s\n", connection.Person1Role, connection.Person1) diff --git a/worker.go b/worker.go index c2b8537..fc2fd9c 100644 --- a/worker.go +++ b/worker.go @@ -13,10 +13,22 @@ type GraphSource struct { Degree int64 // Connections are the details of the connections between the source person and current person. Connections []Collaboration + // IsDest indicates if the current person is the destination person. + IsDest bool +} + +// Reader represents the data that is read by the reader goroutine. +type Reader struct { + // Queue is the next level of people to be explored. + Queue []GraphSource + // FoundDest is true if the destination is found. + FoundDest bool + // DestSource is the details of the destination, it is only set if the "FoundDest" is true. + DestSource GraphSource } // GraphTraversalWorker traverses the nodes of the level provided in the "jobs" channel and writes the nodes of the next level to the "results" channel. -func GraphTraversalWorker(ctx context.Context, ctxCancel context.CancelFunc, destPerson string, vs *VisitedSources, jobs <-chan GraphSource, results chan<- GraphSource) { +func GraphTraversalWorker(ctx context.Context, destPerson string, vs *VisitedSources, jobs <-chan GraphSource, results chan<- GraphSource) { for job := range jobs { // Fetch the person data. personData, err := FetchPerson(job.PersonURL) @@ -63,11 +75,8 @@ func GraphTraversalWorker(ctx context.Context, ctxCancel context.CancelFunc, des } // Check if the destination person has been found. - // If so, print the connections, cancel the context to stop the workers, and return. if member.URL == destPerson { - PrintCollaborations(gs.Connections, gs.Degree) - ctxCancel() - return + gs.IsDest = true } // Add the member to the visited list and write the node to the results channel. @@ -77,6 +86,10 @@ func GraphTraversalWorker(ctx context.Context, ctxCancel context.CancelFunc, des case <-ctx.Done(): return } + + if gs.IsDest { + return + } } } } @@ -115,8 +128,7 @@ func SeperationDegrees(srcPerson string, destPerson string) { // rwGroup is used to wait for the reader (reading from "results" channel) and writer (writing to "results" channel) to finish. rwGroup := &sync.WaitGroup{} - // rwGroupChn is used to send the next level of people to be explored from the reader. - rwGroupChn := make(chan []GraphSource, 1) + rwGroupChn := make(chan Reader, 1) // As the we don't know how many people are in the next level, the size of the // "results" channel is set to 1. That means we need to haver reader always reading from the channel. @@ -126,19 +138,17 @@ func SeperationDegrees(srcPerson string, destPerson string) { defer rwGroup.Done() newQueue := []GraphSource{} - for { - select { - case result, ok := <-results: - if !ok { - rwGroupChn <- newQueue - return - } - newQueue = append(newQueue, result) - - case <-ctx.Done(): + for result := range results { + if result.IsDest { + rwGroupChn <- Reader{FoundDest: true, DestSource: result} + ctxCancel() return } + + newQueue = append(newQueue, result) } + + rwGroupChn <- Reader{Queue: newQueue} }() // Add a goroutine to process the people in the queue. @@ -152,7 +162,7 @@ func SeperationDegrees(srcPerson string, destPerson string) { wg.Add(1) go func() { defer wg.Done() - GraphTraversalWorker(ctx, ctxCancel, destPerson, vs, jobs, results) + GraphTraversalWorker(ctx, destPerson, vs, jobs, results) }() } @@ -170,11 +180,12 @@ func SeperationDegrees(srcPerson string, destPerson string) { rwGroup.Wait() close(rwGroupChn) - select { - case <-ctx.Done(): + res := <-rwGroupChn + if res.FoundDest { + PrintCollaborations(res.DestSource.Connections, res.DestSource.Degree) return - case queue = <-rwGroupChn: } + queue = res.Queue } PrintCollaborations(nil, 0) From d8815a395ffe296cfef45972ff73c72a9f65ad5d Mon Sep 17 00:00:00 2001 From: Milan Thakor Date: Thu, 24 Oct 2024 22:45:39 +0530 Subject: [PATCH 8/8] Update README file. --- README.md | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 6df56a5..8848ebb 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ #Degrees of Separation -With cinema going global these days, every one of the [A-Z]ollywoods are now connected. Use the wealth of data available at [Moviebuff](http://www.moviebuff.com) to see how. +With cinema going global these days, every one of the [A-Z]ollywoods are now connected. Use the wealth of data available at [Moviebuff](http://www.moviebuff.com) to see how. Write a Go program that behaves the following way: @@ -22,7 +22,7 @@ Director: Martin Scorsese Actor: Robert De Niro ``` -Your solution should use the Moviebuff data available to figure out the smallest degree of separation between the two people. +Your solution should use the Moviebuff data available to figure out the smallest degree of separation between the two people. All the inputs should be Moviebuff URLs for their respective people: For Amitabh Bachchan, his page is on http://www.moviebuff.com/amitabh-bachchan and his Moviebuff URL is `amitabh-bachchan`. Please do not attempt to scrape the Moviebuff website - All the data is available on an S3 bucket in an easy to parse JSON format here: `https://data.moviebuff.com/{moviebuff_url}` @@ -42,11 +42,25 @@ http://data.moviebuff.com/martin-scorsese http://data.moviebuff.com/taxi-driver ##Notes -* If you receive HTTP errors when trying to fetch the data, that might be the CDN throttling you. Luckily, Go has some very elegant idioms for rate limiting :) -* There may be a discrepancy in some cases where a movie appears on an actor's list but not vice versa. This usually happens when we edit data while exporting it, so feel free to either ignore these mismatches or handle them in some way. + +- If you receive HTTP errors when trying to fetch the data, that might be the CDN throttling you. Luckily, Go has some very elegant idioms for rate limiting :) +- There may be a discrepancy in some cases where a movie appears on an actor's list but not vice versa. This usually happens when we edit data while exporting it, so feel free to either ignore these mismatches or handle them in some way. Write a program in any language you want (If you're here from Gophercon, use Go :D) that does this. Feel free to make your own input and output format / command line tool / GUI / Webservice / whatever you want. Feel free to hold the dataset in whatever structure you want, but try not to use external databases - as far as possible stick to your langauage without bringing in MySQL/Postgres/MongoDB/Redis/Etc. To submit a solution, fork this repo and send a Pull Request on Github. For any questions or clarifications, raise an issue on this repo and we'll answer your questions as fast as we can. + +# How to use CLI tool to find degrees of separation + +1. Clone this repo. +2. Navigate to the project directory. +3. Install dependencies using the below command: + ``` + go mod download + ``` +4. Run the tool using the below command: + ``` + go run . + ```