From 7c2fa700ecb905c738e17860de52d9274acdc640 Mon Sep 17 00:00:00 2001 From: c-harish Date: Sun, 23 Feb 2025 23:59:05 +0530 Subject: [PATCH 1/5] feat: initial commit with project instructions --- INSTRUCTIONS.md | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 INSTRUCTIONS.md diff --git a/INSTRUCTIONS.md b/INSTRUCTIONS.md new file mode 100644 index 0000000..a986567 --- /dev/null +++ b/INSTRUCTIONS.md @@ -0,0 +1,41 @@ +# Challenge 2015 + +This solution finds the shortest degree of connection between two actors based on their movie collaborations using single source shortest path breadth-first algorithm. + +## Prerequisites + +- Go 1.23.5 or later + +## Installation + +1. Clone the repository: + ```sh + git clone https://github.com/c-harish/toupdate.git + cd challenge2015 + ``` + +2. Install dependencies: + ```sh + go mod tidy + ``` + +## Usage + +1. Run the program: + ```sh + go run main.go + ``` + +2. Enter the source actor's moviebuff URL when prompted: + ``` + source actor : + ``` + +3. Enter the target actor's moviebuff URL when prompted: + ``` + target actor : + ``` + +Refer to https://www.moviebuff.com/ for valid actor_url + +The program will output the degrees of separation and the list movies connecting the two actors. From de2da49bfb61cf0a8d813ce3b7e811180b3c1e3e Mon Sep 17 00:00:00 2001 From: c-harish Date: Mon, 24 Feb 2025 00:07:11 +0530 Subject: [PATCH 2/5] feat: add main.go --- go.mod | 3 + go.sum | 0 main.go | 202 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 205 insertions(+) create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..ff90157 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module challenge2015 + +go 1.23.5 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e69de29 diff --git a/main.go b/main.go new file mode 100644 index 0000000..1d3c1bf --- /dev/null +++ b/main.go @@ -0,0 +1,202 @@ +package main + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "sync" +) + +type Person struct { + Name string `json:"name"` + URL string `json:"url"` + Movies []PersonMovie `json:"movies"` +} + +type PersonMovie struct { + Name string `json:"name"` + URL string `json:"url"` + Role string `json:"role"` +} + +type Movie struct { + Name string `json:"name"` + URL string `json:"url"` + Cast []MoviePerson `json:"cast"` + Crew []MoviePerson `json:"crew"` +} + +type MoviePerson struct { + Name string `json:"name"` + URL string `json:"url"` + Role string `json:"role"` +} + +var cache = make(map[string]interface{}) +var cacheLock = &sync.Mutex{} + +func fetchData(url string) ([]byte, error) { + resp, err := http.Get("https://data.moviebuff.com/" + url) + if err != nil { + return nil, err + } + defer resp.Body.Close() + return io.ReadAll(resp.Body) +} + +type Path struct { + Depth []Step +} + +type Step struct { + Movie string + Role string + Person string +} + +func findConnection(start, end string) (*Path, error) { + // Find Shortest Path from single source using BFS with Queue iteratively + visited := make(map[string]bool) + queue := make([]Path, 0) + + queue = append(queue, Path{Depth: []Step{}}) + visited[start] = true + + for len(queue) > 0 { + currentPath := queue[0] + queue = queue[1:] + + currentPerson := start + if len(currentPath.Depth) > 0 { + currentPerson = currentPath.Depth[len(currentPath.Depth)-1].Person + } + + // Check if data exists in cache before making API call + var person Person + data, exists := cache[currentPerson] + if !exists { + rawData, err := fetchDataAsync(currentPerson) + if err != nil { + continue + } + if err := json.Unmarshal(rawData, &person); err != nil { + continue + } + // Store the response in Cache for future use + // Using Lock to make map thread-safe + cacheLock.Lock() + cache[currentPerson] = person + cacheLock.Unlock() + } else { + person = data.(Person) + } + + var wg sync.WaitGroup + movieChan := make(chan Movie, len(person.Movies)) + + for _, movie := range person.Movies { + wg.Add(1) + // Fetching movie data asynchronously by spawning goroutine + go func(movie PersonMovie) { + defer wg.Done() + var movieData Movie + data, exists := cache[movie.URL] + if !exists { + rawData, err := fetchDataAsync(movie.URL) + if err != nil { + return + } + if err := json.Unmarshal(rawData, &movieData); err != nil { + return + } + cacheLock.Lock() + cache[movie.URL] = movieData + cacheLock.Unlock() + } else { + movieData = data.(Movie) + } + movieChan <- movieData + }(movie) + } + + go func() { + wg.Wait() + close(movieChan) + }() + + for movieData := range movieChan { + // Check in both cast and crew of the movie for connections + for _, p := range append(movieData.Cast, movieData.Crew...) { + if visited[p.URL] { + continue + } + + visited[p.URL] = true + + newPath := Path{ + Depth: append(currentPath.Depth, Step{ + Movie: movieData.Name, + Role: p.Role, + Person: p.URL, + }), + } + + if p.URL == end { + return &newPath, nil + } + + queue = append(queue, newPath) + } + } + } + + return nil, fmt.Errorf("connection error") +} + +func fetchDataAsync(url string) ([]byte, error) { + dataChan := make(chan []byte, 1) + errChan := make(chan error, 1) + + go func() { + data, err := fetchData(url) + if err != nil { + errChan <- err + return + } + dataChan <- data + }() + + select { + case data := <-dataChan: + return data, nil + case err := <-errChan: + return nil, err + } +} + +func main() { + var person1, person2 string + + fmt.Print("source actor : ") + fmt.Scanf("%s", &person1) + fmt.Print("target actor : ") + fmt.Scanf("%s", &person2) + path, err := findConnection(person1, person2) + if err != nil { + fmt.Println("Error:", err) + os.Exit(1) + } + + fmt.Printf("Degrees of Separation: %d\n\n", len(path.Depth)) + for i, step := range path.Depth { + fmt.Printf("%d. Movie: %s\n", i+1, step.Movie) + if i == 0 { + fmt.Printf(" %s: %s\n", step.Role, person1) + } else { + fmt.Printf(" %s: %s\n", path.Depth[i-1].Role, path.Depth[i-1].Person) + } + fmt.Printf(" %s: %s\n", step.Role, step.Person) + } +} From 05b7c5efe02a843eb368a6dce3551bfd52c22abe Mon Sep 17 00:00:00 2001 From: c-harish Date: Mon, 24 Feb 2025 00:08:37 +0530 Subject: [PATCH 3/5] nit: delete go.sum --- go.sum | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 go.sum diff --git a/go.sum b/go.sum deleted file mode 100644 index e69de29..0000000 From 4bdbfa03129f14d03456f304e9f81ea2bf5e22cb Mon Sep 17 00:00:00 2001 From: c-harish Date: Mon, 24 Feb 2025 00:16:29 +0530 Subject: [PATCH 4/5] nit: format lines --- main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.go b/main.go index 1d3c1bf..b9f8c8b 100644 --- a/main.go +++ b/main.go @@ -189,7 +189,7 @@ func main() { os.Exit(1) } - fmt.Printf("Degrees of Separation: %d\n\n", len(path.Depth)) + fmt.Printf("\nDegrees of Separation: %d\n\n", len(path.Depth)) for i, step := range path.Depth { fmt.Printf("%d. Movie: %s\n", i+1, step.Movie) if i == 0 { From fdf353088a879902640c2f0410fa3291dd39f3d7 Mon Sep 17 00:00:00 2001 From: c-harish Date: Mon, 24 Feb 2025 01:44:07 +0530 Subject: [PATCH 5/5] nit: fix link in instructions --- INSTRUCTIONS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/INSTRUCTIONS.md b/INSTRUCTIONS.md index a986567..722998f 100644 --- a/INSTRUCTIONS.md +++ b/INSTRUCTIONS.md @@ -10,7 +10,7 @@ This solution finds the shortest degree of connection between two actors based o 1. Clone the repository: ```sh - git clone https://github.com/c-harish/toupdate.git + git clone https://github.com/c-harish/challenge2015.git cd challenge2015 ```