diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bf0824e --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.log \ No newline at end of file diff --git a/README.md b/README.md index 6df56a5..adb4a73 100644 --- a/README.md +++ b/README.md @@ -50,3 +50,14 @@ Write a program in any language you want (If you're here from Gophercon, use Go 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. + +### Steps to Run +1. cd cmd/moviebuff +2. **Run the program with the following command:** + ```bash + $ go run . amitabh-bachchan john-abraham + ``` + Replace `amitabh-bachchan` and `john-abraham` with the names of the actors you want to process. + + + ``` diff --git a/cmd/moviebuff/main.go b/cmd/moviebuff/main.go new file mode 100644 index 0000000..d412ce6 --- /dev/null +++ b/cmd/moviebuff/main.go @@ -0,0 +1,30 @@ +package main + +import ( + services "challenge2015/internal/domain/services" + "fmt" + "os" +) + +func main() { + args := os.Args + + if len(args) != 3 { + fmt.Println("provide atleast two arguments") + return + } + if args[1] == args[2] { + fmt.Println("Please provide different actors") + return + } + services.FirstActor = args[1] + services.SecondActor = args[2] + + services.ActorListForFirst = append(services.ActorListForFirst, args[1]) + services.ActorListForSecond = append(services.ActorListForSecond, args[2]) + + err := services.SmallestDegreeOfSeparation() + if err != nil { + fmt.Println("Error in finding smallest degree of separation: ", err) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..b43870d --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module challenge2015 + +go 1.21 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e69de29 diff --git a/internal/domain/models/models.go b/internal/domain/models/models.go new file mode 100644 index 0000000..1a17d45 --- /dev/null +++ b/internal/domain/models/models.go @@ -0,0 +1,26 @@ +package moviebuff + +type Data struct { + Cast []Data `json:"cast"` + Crew []Data `json:"crew"` + Movies []Data `json:"movies"` + Name string `json:"name"` + Role string `json:"role"` + Type string `json:"type"` + Url string `json:"url"` +} + +type ActorData struct { + Url string `json:"url"` + Type string `json:"type"` + Name string `json:"name"` + Movies []Data `json:"movies"` +} + +type MovieData struct { + Url string `json:"url"` + Type string `json:"type"` + Name string `json:"name"` + Cast []Data `json:"cast"` + Crew []Data `json:"crew"` +} diff --git a/internal/domain/services/moviebuff.go b/internal/domain/services/moviebuff.go new file mode 100644 index 0000000..038b478 --- /dev/null +++ b/internal/domain/services/moviebuff.go @@ -0,0 +1,285 @@ +package services + +import ( + domainmodels "challenge2015/internal/domain/models" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" +) + +var ( + FirstActor string + SecondActor string + // actor->movie->role + actorsDataMap = map[string]map[string]string{} + // movie->cast + movieDataMap = map[string][]domainmodels.Data{} + visitedActorsMap = map[string]bool{} + firstActorMoviesVisited = map[string]bool{} + secondActorMoviesVisited = map[string]bool{} + ActorListForFirst = []string{} + ActorListForSecond = []string{} + moviesListForFirstActor = []string{} + moviesListForSecondActor = []string{} +) + +func SmallestDegreeOfSeparation() error { + + commonMovie, err := tryFindingCommonMovie() + if err != nil { + return fmt.Errorf("no connection found between the actors") + } + if commonMovie != "" { + findDegreeOfSeparation(FirstActor, SecondActor) + return nil + } + + err = appendCastMemberToActorsList() + if err != nil { + return fmt.Errorf("no connection found") + } + + return SmallestDegreeOfSeparation() +} + +// finds common movie between two actors and returns if any. +func tryFindingCommonMovie() (string, error) { + if len(ActorListForFirst) == 0 || len(ActorListForSecond) == 0 { + return "", errors.New("try finding common movie: no actors found") + } + + for len(ActorListForFirst) > 0 && len(ActorListForSecond) > 0 { + + firstActor := ActorListForFirst[0] + ActorListForFirst = ActorListForFirst[1:] + actorsDataMap[firstActor] = map[string]string{} + + secondActor := ActorListForSecond[0] + ActorListForSecond = ActorListForSecond[1:] + actorsDataMap[secondActor] = map[string]string{} + + moviesDataForFirstActor, err := getActorsData(firstActor) + if err != nil { + return "", nil + } + moviesDataForSecondActor, err := getActorsData(secondActor) + if err != nil { + return "", nil + } + + for _, movie := range moviesDataForFirstActor.Movies { + actorsDataMap[firstActor][movie.Url] = movie.Role + if _, ok := firstActorMoviesVisited[movie.Url]; !ok { + firstActorMoviesVisited[movie.Url] = true + moviesListForFirstActor = append(moviesListForFirstActor, movie.Url) + } + } + + for _, movie := range moviesDataForSecondActor.Movies { + actorsDataMap[secondActor][movie.Url] = movie.Role + if _, ok := secondActorMoviesVisited[movie.Url]; !ok { + secondActorMoviesVisited[movie.Url] = true + moviesListForSecondActor = append(moviesListForSecondActor, movie.Url) + } + } + + commonMovie := searchCommonMovie(firstActorMoviesVisited, secondActorMoviesVisited) + if commonMovie != "" { + return commonMovie, nil + } + } + + return "", nil +} + +// appends all cast members to actors list for first and second actor. +func appendCastMemberToActorsList() error { + if len(moviesListForFirstActor) == 0 || len(moviesListForSecondActor) == 0 { + return errors.New("try finding common cast member: empty movie lists") + } + + for len(moviesListForFirstActor) > 0 && len(moviesListForSecondActor) > 0 { + + firstActorMovie := moviesListForFirstActor[0] + moviesListForFirstActor = moviesListForFirstActor[1:] + + secondActorMovie := moviesListForSecondActor[0] + moviesListForSecondActor = moviesListForSecondActor[1:] + + moviesDataForFirstActor, err := getMovieData(firstActorMovie) + if err != nil { + return nil + } + moviesDataForSecondActor, err := getMovieData(secondActorMovie) + if err != nil { + return nil + } + + firstActorsCast := []domainmodels.Data{} + firstActorsCast = append(firstActorsCast, moviesDataForFirstActor.Cast...) + firstActorsCast = append(firstActorsCast, moviesDataForFirstActor.Crew...) + movieDataMap[firstActorMovie] = append(movieDataMap[firstActorMovie], firstActorsCast...) + + for _, cast := range firstActorsCast { + if _, ok := visitedActorsMap[cast.Name]; !ok { + visitedActorsMap[cast.Name] = true + ActorListForFirst = append(ActorListForFirst, cast.Url) + } + } + + secondActorsCast := []domainmodels.Data{} + secondActorsCast = append(secondActorsCast, moviesDataForSecondActor.Cast...) + secondActorsCast = append(secondActorsCast, moviesDataForSecondActor.Crew...) + movieDataMap[secondActorMovie] = append(movieDataMap[secondActorMovie], secondActorsCast...) + + for _, cast := range secondActorsCast { + if _, ok := visitedActorsMap[cast.Name]; !ok { + visitedActorsMap[cast.Name] = true + ActorListForSecond = append(ActorListForSecond, cast.Url) + } + } + + } + + return nil +} + +type QueueItem struct { + actor string + level int + commonActors []string +} + +// finds degree of separation between the actors and returns. +func findDegreeOfSeparation(firstActor, secondActor string) { + visited := make(map[string]bool) + queue := make([]QueueItem, 0) + + queue = append(queue, QueueItem{firstActor, 0, []string{firstActor}}) + visited[firstActor] = true + + for len(queue) > 0 { + currentItem := queue[0] + queue = queue[1:] + + currentActor := currentItem.actor + currentLevel := currentItem.level + currentcommonActors := currentItem.commonActors + + if currentActor == secondActor { + print(currentcommonActors) + return + } + + for movie := range actorsDataMap[currentActor] { + for _, nextActor := range movieDataMap[movie] { + if !visited[nextActor.Name] { + visited[nextActor.Name] = true + nextcommonActors := append(currentcommonActors, nextActor.Url) + queue = append(queue, QueueItem{nextActor.Url, currentLevel + 1, nextcommonActors}) + } + } + } + } +} + +// output. +func print(commonActors []string) { + fmt.Println("Degrees of separation: ", len(commonActors)-1) + for i := 0; i < len(commonActors)-1; i++ { + fmt.Println() + movie, role1, role2 := findCommonMovie(actorsDataMap[commonActors[i]], actorsDataMap[commonActors[i+1]]) + fmt.Printf("%d. Movie: %s\n", i+1, movie) + fmt.Printf("%s: %s\n", role1, commonActors[i]) + fmt.Printf("%s: %s\n", role2, commonActors[i+1]) + } +} + +// returns common movie from first and second actors movies visited map. +func searchCommonMovie(firstActorMoviesVisited, secondActorMoviesVisited map[string]bool) string { + for k := range firstActorMoviesVisited { + if _, ok := secondActorMoviesVisited[k]; ok { + if len(movieDataMap[k]) > 1 { + return k + } + } + } + return "" +} + +func findCommonMovie(mv1, mv2 map[string]string) (string, string, string) { + for m1, r1 := range mv1 { + for m2, r2 := range mv2 { + if m1 == m2 { + return m1, r1, r2 + } + } + } + return "", "", "" +} + +func getActorsData(actor string) (domainmodels.ActorData, error) { + data, err := getData(actor) + if err != nil { + return domainmodels.ActorData{}, err + } + if data.Type != "Person" { + return domainmodels.ActorData{}, errors.New("get actors data: invalid data") + } + return domainmodels.ActorData{ + Url: data.Url, + Type: data.Type, + Name: data.Name, + Movies: data.Movies, + }, nil +} + +func getMovieData(movie string) (domainmodels.MovieData, error) { + data, err := getData(movie) + if err != nil { + return domainmodels.MovieData{}, err + } + if data.Type != "Movie" { + return domainmodels.MovieData{}, errors.New("get movie data: invalid data") + } + return domainmodels.MovieData{ + Url: data.Url, + Type: data.Type, + Name: data.Name, + Cast: data.Cast, + Crew: data.Crew, + }, nil +} + +func getData(input string) (domainmodels.Data, error) { + url := fmt.Sprintf("http://data.moviebuff.com/%s", input) + // Make the HTTP request + response, err := http.Get(url) + if err != nil { + fmt.Println("Error:", err) + return domainmodels.Data{}, err + } + defer response.Body.Close() + + if response.StatusCode != 200 { + return domainmodels.Data{}, errors.New("Invalid") + } + + // Read the response body + body, err := io.ReadAll(response.Body) + if err != nil { + fmt.Println("Error reading response body:", err) + return domainmodels.Data{}, err + } + + var data domainmodels.Data + err = json.Unmarshal(body, &data) + if err != nil { + fmt.Println("Error unmarshalling JSON:", err) + return domainmodels.Data{}, err + } + + return data, nil +}