diff --git a/README.md b/README.md index 6df56a5..eb172ba 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,23 @@ -#Degrees of Separation +# Steps to run the program -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. +## Navigate to the degrees folder +``` +$ cd degrees +``` + +## Compile the program +``` +$ go build -o degrees ./main.go -Write a Go program that behaves the following way: +``` +## Run the executable by providing person urls as input +``` +$ ./degrees amitabh-bachchan robert-de-niro ``` -$ degrees amitabh-bachchan robert-de-niro +## Example of output +``` Degrees of Separation: 3 1. Movie: The Great Gatsby @@ -20,33 +31,5 @@ Director: Martin Scorsese 3. Movie: Taxi Driver 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. -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}` - -To solve the example above, your solution would fetch at least the following: -http://data.moviebuff.com/amitabh-bachchan - -http://data.moviebuff.com/the-great-gatsby - -http://data.moviebuff.com/leonardo-dicaprio - -http://data.moviebuff.com/the-wolf-of-wall-street - -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. - -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. +``` diff --git a/degrees/datastructs/structs.go b/degrees/datastructs/structs.go new file mode 100644 index 0000000..26b319b --- /dev/null +++ b/degrees/datastructs/structs.go @@ -0,0 +1,25 @@ +package datastructs + +type General struct { + Url string + Name string +} + +type Entity struct { + General + Type string +} +type Info struct { + General + Role string +} + +type Movie struct { + Entity + Cast []Info +} + +type Person struct { + Entity + Movies []Info +} diff --git a/degrees/degreecalculator/calculate.go b/degrees/degreecalculator/calculate.go new file mode 100644 index 0000000..4ce33a0 --- /dev/null +++ b/degrees/degreecalculator/calculate.go @@ -0,0 +1,197 @@ +package degreecalculator + +import ( + "degrees/moviebuffclient" + "fmt" + "sync" + "sync/atomic" +) + +type void struct{} + +type node struct { + parent *node + parentRole string + person +} + +type person struct { + name string + url string + role string + movie string +} + +type Result struct { + Level int + Node *node + Err error +} + +var pregL, mregL sync.Mutex + +func Calculate(p1, p2 string) (int, *node, error) { + ch := make(chan Result) + // maintains the list of visited urls in the search process + registry := make(map[string]void) + registry[p1] = void{} + movieRegistry := make(map[string]void) + parentNode := &node{person: person{url: p1}} + list := []*node{parentNode} + go traverse(ch, 1, p2, list, registry, movieRegistry) + res := <-ch + return res.Level, res.Node, res.Err +} + +func traverse(ch chan Result, level int, destinationUrl string, list []*node, registry map[string]void, movieRegistry map[string]void) { + + if len(list) == 0 { + ch <- Result{-1, nil, nil} + return + } + + terminate := new(atomic.Bool) + terminate.Store(false) + + var nextLevelList []*node + + wg := sync.WaitGroup{} + maxGoroutines := make(chan struct{}, 150) + defer close(maxGoroutines) + + // fetch direct assocaited persons of all the person in the current level + for _, p := range list { + if terminate.Load() { + return + } + wg.Add(1) + maxGoroutines <- struct{}{} + + go func(p *node) { + defer wg.Done() + defer func() { + <-maxGoroutines + }() + + // fetch person info + personInfo, err := moviebuffclient.GetPersonInfo(p.url) + if err != nil { + ch <- Result{-2, nil, err} + return + } + + if p.parent == nil { + // update person info in the node + p.name = personInfo.Name + } + + for _, m := range personInfo.Movies { + // check if the movie is already visited + mregL.Lock() + if _, ok := movieRegistry[m.Url]; ok { + mregL.Unlock() + continue + } + // add the movie to the registry + movieRegistry[m.Url] = void{} + mregL.Unlock() + + // fetch movie info + movieInfo, err := moviebuffclient.GetMovieInfo(m.Url) + if err != nil { + ch <- Result{-2, nil, err} + return + } + + parentRole := m.Role + + for _, c := range movieInfo.Cast { + // generate a new node + newNode := &node{ + p, + parentRole, + person{ + name: c.Name, + url: c.Url, + role: c.Role, + movie: movieInfo.Name, + }, + } + + // check if the destination url is reached + if c.Url == destinationUrl { + if terminate.Load() { + return + } + ch <- Result{level, newNode, nil} // complete the function + terminate.Store(true) + return + } + // check if the person is already visited + pregL.Lock() + if _, ok := registry[c.Url]; !ok { + // add the person to the registry + registry[c.Url] = void{} + // add the person to the next level + nextLevelList = append(nextLevelList, newNode) + pregL.Unlock() + continue + } + pregL.Unlock() + + } + } + }(p) + } + wg.Wait() + + traverse(ch, level+1, destinationUrl, nextLevelList, registry, movieRegistry) +} + +func PrintRespose(level int, n *node, err error) { + if checkForNonTrivialBehavior(level, err) { + return + } + var entries []string + fmt.Println("Degrees of seperation: ", level) + count := level + cur := n + for cur.parent != nil { + entries = append(entries, fmt.Sprintf(`%d. Movie: %s +%s: %s +%s: %s%s`, count, cur.movie, cur.parentRole, cur.parent.name, cur.role, cur.name, "\n")) + cur = cur.parent + count-- + } + for i := len(entries) - 1; i >= 0; i-- { + fmt.Println(entries[i]) + } +} + +func PrintResponseInReverse(level int, n *node, err error) { + if checkForNonTrivialBehavior(level, err) { + return + } + fmt.Println("Degrees of seperation: ", level) + count := 1 + cur := n + for cur.parent != nil { + fmt.Printf(`%d. Movie: %s +%s: %s +%s: %s%s`, count, cur.movie, cur.role, cur.name, cur.parentRole, cur.parent.name, "\n") + cur = cur.parent + count++ + } +} + +func checkForNonTrivialBehavior(level int, err error) bool { + if err != nil { + fmt.Println("Error in calculating degree of seperation:", err) + return true + } + if level == -1 { + fmt.Println("No degree of seperation found") + return true + } + return false +} diff --git a/degrees/go.mod b/degrees/go.mod new file mode 100644 index 0000000..8af909b --- /dev/null +++ b/degrees/go.mod @@ -0,0 +1,3 @@ +module degrees + +go 1.19 diff --git a/degrees/main.go b/degrees/main.go new file mode 100644 index 0000000..d6bfc58 --- /dev/null +++ b/degrees/main.go @@ -0,0 +1,54 @@ +package main + +import ( + "degrees/degreecalculator" + "degrees/moviebuffclient" + "fmt" + "os" +) + +func main() { + // take input as commnad line arguments + input := os.Args + if len(input) < 3 { + fmt.Println("Please provide two person urls as command line arguments") + return + } + person1Url := input[1] + person2Url := input[2] + + // validate the input + dir, err := validateInputAndProvideFlowDirection(person1Url, person2Url) + if err != nil { + fmt.Println(fmt.Errorf("error while validating input: %s", err)) + return + } + + // calculate the degree of separation + if dir { + separation, chainInfo, err := degreecalculator.Calculate(person1Url, person2Url) + degreecalculator.PrintRespose(separation, chainInfo, err) + return + } + separation, chainInfo, err := degreecalculator.Calculate(person2Url, person1Url) + degreecalculator.PrintResponseInReverse(separation, chainInfo, err) + +} + +func validateInputAndProvideFlowDirection(person1Url string, person2Url string) (bool, error) { + if person1Url == person2Url { + return false, fmt.Errorf("given urls are same. Please provide two different urls") + } + p1Info, err := moviebuffclient.GetPersonInfo(person1Url) + if err != nil { + return false, err + } + p2Info, err := moviebuffclient.GetPersonInfo(person2Url) + if err != nil { + return false, err + } + if len(p1Info.Movies) < len(p2Info.Movies) { + return true, nil + } + return false, nil +} diff --git a/degrees/moviebuffclient/clients.go b/degrees/moviebuffclient/clients.go new file mode 100644 index 0000000..ec941eb --- /dev/null +++ b/degrees/moviebuffclient/clients.go @@ -0,0 +1,52 @@ +package moviebuffclient + +import ( + "degrees/datastructs" + "encoding/json" + "fmt" + "net/http" + "time" +) + +var c = &http.Client{ + Timeout: 30 * time.Second, +} + +func MakeHttpReq(suffix string) (*http.Response, error) { + httpUrl := fmt.Sprintf("http://data.moviebuff.com/%s", suffix) + req, err := http.NewRequest("GET", httpUrl, nil) + if err != nil { + return nil, err + } + res, err := c.Do(req) + return res, err +} + +func GetPersonInfo(personUrl string) (*datastructs.Person, error) { + res, err := MakeHttpReq(personUrl) + if err != nil { + return nil, err + } + defer res.Body.Close() + var data datastructs.Person + if res.StatusCode != 200 { + return &data, nil + } + + err = json.NewDecoder(res.Body).Decode(&data) + return &data, err +} + +func GetMovieInfo(movieUrl string) (*datastructs.Movie, error) { + res, err := MakeHttpReq(movieUrl) + if err != nil { + return nil, err + } + defer res.Body.Close() + var data datastructs.Movie + if res.StatusCode != 200 { + return &data, nil + } + err = json.NewDecoder(res.Body).Decode(&data) + return &data, err +}