diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 000000000..40067e940 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,15 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Debug Current Go File", + "type": "go", + "request": "launch", + "mode": "auto", + "program": "${file}", + "env": {}, + "args": [], + "showLog": true + } + ] +} \ No newline at end of file diff --git a/README.md b/README.md index f1c342f65..3a14991c8 100644 --- a/README.md +++ b/README.md @@ -1,41 +1,64 @@ -# Real Image Challenge 2016 -In the cinema business, a feature film is usually provided to a regional distributor based on a contract for exhibition in a particular geographical territory. +# Go Based Distribution Program -Each authorization is specified by a combination of included and excluded regions. For example, a distributor might be authorzied in the following manner: -``` -Permissions for DISTRIBUTOR1 -INCLUDE: INDIA -INCLUDE: UNITEDSTATES -EXCLUDE: KARNATAKA-INDIA -EXCLUDE: CHENNAI-TAMILNADU-INDIA -``` -This allows `DISTRIBUTOR1` to distribute in any city inside the United States and India, *except* cities in the state of Karnataka (in India) and the city of Chennai (in Tamil Nadu, India). + -At this point, asking your program if `DISTRIBUTOR1` has permission to distribute in `CHICAGO-ILLINOIS-UNITEDSTATES` should get `YES` as the answer, and asking if distribution can happen in `CHENNAI-TAMILNADU-INDIA` should of course be `NO`. Asking if distribution is possible in `BANGALORE-KARNATAKA-INDIA` should also be `NO`, because the whole state of Karnataka has been excluded. +## Overview -Sometimes, a distributor might split the work of distribution amount smaller sub-distiributors inside their authorized geographies. For instance, `DISTRIBUTOR1` might assign the following permissions to `DISTRIBUTOR2`: + -``` -Permissions for DISTRIBUTOR2 < DISTRIBUTOR1 -INCLUDE: INDIA -EXCLUDE: TAMILNADU-INDIA -``` -Now, `DISTRIBUTOR2` can distribute the movie anywhere in `INDIA`, except inside `TAMILNADU-INDIA` and `KARNATAKA-INDIA` - `DISTRIBUTOR2`'s permissions are always a subset of `DISTRIBUTOR1`'s permissions. It's impossible/invalid for `DISTRIBUTOR2` to have `INCLUDE: CHINA`, for example, because `DISTRIBUTOR1` isn't authorized to do that in the first place. +This command-line tool, written in Go, allows you to manage distributors and their permissions based on geographic regions. With this program, you can: -If `DISTRIBUTOR2` authorizes `DISTRIBUTOR3` to handle just the city of Hubli, Karnataka, India, for example: -``` -Permissions for DISTRIBUTOR3 < DISTRIBUTOR2 < DISTRIBUTOR1 -INCLUDE: HUBLI-KARNATAKA-INDIA -``` -Again, `DISTRIBUTOR2` cannot authorize `DISTRIBUTOR3` with a region that they themselves do not have access to. + + +- **Create Distributors:** Define distributors with specific authorized regions. + +- **Add Regions:** Add new regions to a distributor’s authorized list. + +- **Exclude Regions:** Exclude certain regions from a distributor’s effective permissions. + +- **Establish Parent-Child Relationships:** Set up hierarchical relationships so that a child's permissions are always a subset of its parent's. + +- **Check Permissions:** Verify if a distributor is permitted to operate in a given region. + + + +## Getting Started -We've provided a CSV with the list of all countries, states and cities in the world that we know of - please use the data mentioned there for this program. *The codes you see there may be different from what you see here, so please always use the codes in the CSV*. This Readme is only an example. + -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. +### Prerequisites -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. +- **Go:** Ensure you have Go installed (version 1.16 or later). +- **CSV File:** The program requires a `cities.csv` file (located in the `internal/assets` folder) with region data. The CSV should include columns like: +- City Code + +- Province Code + +- Country Code + +- City Name + +- Province Name + +- Country Name + +### How to Run? + +1. **Build the Program:** + +```bash +cd cmd +go build main +``` + +1. **Run the Program:** + + +```bash + ./main +``` diff --git a/cmd/main.go b/cmd/main.go new file mode 100644 index 000000000..c77e249ba --- /dev/null +++ b/cmd/main.go @@ -0,0 +1,225 @@ +package main + +import ( + "CHALLENGE2016/internal/models/distributor" + "CHALLENGE2016/internal/models/region" + "bufio" + "encoding/csv" + "fmt" + "os" + "strings" +) + +// initializeRegions reads the CSV file and adds regions to the RegionManager +func initializeRegions(rm *region.RegionManager, csvFile string) error { + file, err := os.Open(csvFile) + if err != nil { + return fmt.Errorf("failed to open CSV file: %w", err) + } + defer file.Close() + + reader := csv.NewReader(file) + lines, err := reader.ReadAll() + if err != nil { + return fmt.Errorf("failed to read CSV file: %w", err) + } + + for i, line := range lines { + if i == 0 { + continue + } + if len(line) < 6 { + continue + } + cityName := line[3] + provinceName := line[4] + countryName := line[5] + + regionPath := fmt.Sprintf("%s-%s-%s", countryName, provinceName, cityName) + rm.AddRegion(regionPath) + } + return nil +} + +func main() { + // Initialize RegionManager and load regions from CSV + rm := region.NewRegionManager() + err := initializeRegions(rm, "../internal/assets/cities.csv") + if err != nil { + fmt.Println("Error initializing regions:", err) + return + } + + distributors := make(map[string]*distributor.Distributor) + + scanner := bufio.NewScanner(os.Stdin) + + for { + fmt.Println("\n----- Menu -----") + fmt.Println("1. Create Distributor") + fmt.Println("2. Add Region to Distributor") + fmt.Println("3. Exclude Region from Distributor") + fmt.Println("4. Add Parent to Distributor") + fmt.Println("5. Check Distributor Permission") + fmt.Println("6. List Distributors") + fmt.Println("7. Exit") + fmt.Print("Enter option: ") + + if !scanner.Scan() { + break + } + option := strings.TrimSpace(scanner.Text()) + + switch option { + case "1": + // Create a new distributor + fmt.Print("Enter distributor ID (unique string): ") + scanner.Scan() + id := strings.TrimSpace(scanner.Text()) + + fmt.Print("Enter distributor Name: ") + scanner.Scan() + name := strings.TrimSpace(scanner.Text()) + + // Input authorized regions as comma-separated region paths + fmt.Print("Enter authorized region paths (comma separated, e.g. 'India-Tamil Nadu-Keelakarai, India-Jammu and Kashmir-Punch'): ") + scanner.Scan() + regionsInput := scanner.Text() + regionPaths := strings.Split(regionsInput, ",") + + authRegions := make(map[string]*region.Region) + for _, rp := range regionPaths { + rp = strings.TrimSpace(rp) + if rp == "" { + continue + } + if r, exists := rm.GetRegion(rp); exists { + authRegions[rp] = r + } else { + fmt.Printf("Region '%s' not found. Skipping.\n", rp) + } + } + + dist := distributor.AddDistributor(id, name, authRegions) + distributors[id] = dist + fmt.Println("Distributor created.") + + case "2": + // Add a region to a distributor. + fmt.Print("Enter distributor ID: ") + scanner.Scan() + id := strings.TrimSpace(scanner.Text()) + dist, ok := distributors[id] + if !ok { + fmt.Println("Distributor not found.") + break + } + + fmt.Print("Enter region path to add (e.g. 'India-Tamil Nadu-Keelakarai'): ") + scanner.Scan() + rp := strings.TrimSpace(scanner.Text()) + if r, exists := rm.GetRegion(rp); exists { + if err := dist.AddRegion(r); err != nil { + fmt.Println("Error:", err) + } else { + fmt.Println("Region added to distributor.") + } + } else { + fmt.Println("Region not found.") + } + + case "3": + // Exclude a region from a distributor + fmt.Print("Enter distributor ID: ") + scanner.Scan() + id := strings.TrimSpace(scanner.Text()) + dist, ok := distributors[id] + if !ok { + fmt.Println("Distributor not found.") + break + } + + fmt.Print("Enter region path to exclude: ") + scanner.Scan() + rp := strings.TrimSpace(scanner.Text()) + if r, exists := rm.GetRegion(rp); exists { + dist.ExcludeRegion(r) + fmt.Println("Region excluded from distributor.") + } else { + fmt.Println("Region not found.") + } + + case "4": + // Add a parent distributor + fmt.Print("Enter child distributor ID: ") + scanner.Scan() + childID := strings.TrimSpace(scanner.Text()) + childDist, ok := distributors[childID] + if !ok { + fmt.Println("Child distributor not found.") + break + } + + fmt.Print("Enter parent distributor ID: ") + scanner.Scan() + parentID := strings.TrimSpace(scanner.Text()) + parentDist, ok := distributors[parentID] + if !ok { + fmt.Println("Parent distributor not found.") + break + } + + childDist.AddParent(parentDist) + fmt.Println("Parent added to distributor.") + + case "5": + + // Check if a distributor has permission for a given region. + fmt.Print("Enter distributor ID: ") + scanner.Scan() + id := strings.TrimSpace(scanner.Text()) + dist, ok := distributors[id] + if !ok { + fmt.Println("Distributor not found.") + break + } + + fmt.Print("Enter region path to check (e.g. 'India-Tamil Nadu-Keelakarai'): ") + scanner.Scan() + rp := strings.TrimSpace(scanner.Text()) + if r, exists := rm.GetRegion(rp); exists { + if dist.HasPermission(r) { + fmt.Println("Permission granted!") + } else { + fmt.Println("Permission denied!") + } + } else { + fmt.Println("Region not found.") + } + + case "6": + // List all distributors. + fmt.Println("Listing Distributors:") + for id, dist := range distributors { + fmt.Println("--------------------------------") + fmt.Printf("ID: %s, Name: %s\n", id, dist.Name) + fmt.Print("\n Excluded Regions: ") + for regID := range dist.ExcludedRegions { + fmt.Printf("%s ", regID) + } + fmt.Print("\n Effective Authorized Regions: ") + for regID := range dist.EffectiveAuthorizedRegions { + fmt.Printf("%s ", regID) + } + fmt.Println("\n--------------------------------") + } + + case "7": + fmt.Println("Exiting.") + return + + default: + fmt.Println("Invalid option. Please try again.") + } + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 000000000..4950c656f --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module CHALLENGE2016 + +go 1.24.0 diff --git a/cities.csv b/internal/assets/cities.csv similarity index 100% rename from cities.csv rename to internal/assets/cities.csv diff --git a/internal/models/distributor/distributor.go b/internal/models/distributor/distributor.go new file mode 100644 index 000000000..e1cb0f5ef --- /dev/null +++ b/internal/models/distributor/distributor.go @@ -0,0 +1,180 @@ +package distributor + +import ( + "CHALLENGE2016/internal/models/region" + "fmt" + "strings" +) + +type Distributor struct { + ID string + + Name string + + AuthorizedRegions map[string]*region.Region + + ExcludedRegions map[string]*region.Region + + ParentDistributors map[string]*Distributor + + Children map[string]*Distributor + + EffectiveAuthorizedRegions map[string]*region.Region +} + +func NewDistributor(id, name string) *Distributor { + return &Distributor{ + ID: id, + Name: name, + AuthorizedRegions: make(map[string]*region.Region), + ExcludedRegions: make(map[string]*region.Region), + ParentDistributors: make(map[string]*Distributor), + Children: make(map[string]*Distributor), + EffectiveAuthorizedRegions: make(map[string]*region.Region), + } +} + +// AddDistributor creates a distributor with an initial set of authorized regions. +func AddDistributor(id, name string, authorizedRegions map[string]*region.Region) *Distributor { + d := NewDistributor(id, name) + for key, r := range authorizedRegions { + d.AuthorizedRegions[key] = r + } + d.updateEffectiveAuthorizedRegions() + return d +} + +// allowedByAllParents checks if a candidate region is allowed by all parent distributors. +func allowedByAllParents(candidate *region.Region, parents map[string]*Distributor) bool { + for _, parent := range parents { + // Check parent's exclusions. + for _, ex := range parent.ExcludedRegions { + if isSubregion(candidate, ex) { + return false + } + } + // Check that candidate is allowed by at least one of parent's effective regions. + allowed := false + for _, pr := range parent.EffectiveAuthorizedRegions { + if isSubregion(candidate, pr) { + allowed = true + break + } + } + if !allowed { + return false + } + } + return true +} + +// AddRegion adds a region to the distributor's AuthorizedRegions. +func (d *Distributor) AddRegion(r *region.Region) error { + if !allowedByAllParents(r, d.ParentDistributors) { + return fmt.Errorf("cannot add region %s: not authorized by all parent distributors", r.FullPath) + } + d.AuthorizedRegions[r.FullPath] = r + d.updateEffectiveAuthorizedRegions() + d.propagateEffectiveUpdate() + return nil +} + +func (d *Distributor) RemoveRegion(r *region.Region) { + delete(d.AuthorizedRegions, r.FullPath) + d.updateEffectiveAuthorizedRegions() + d.propagateEffectiveUpdate() +} + +func (d *Distributor) ExcludeRegion(r *region.Region) { + d.ExcludedRegions[r.FullPath] = r + d.updateEffectiveAuthorizedRegions() + d.propagateEffectiveUpdate() +} + +func (child *Distributor) AddParent(parent *Distributor) { + if parent == nil { + return + } + child.ParentDistributors[parent.ID] = parent + parent.Children[child.ID] = child + child.updateEffectiveAuthorizedRegions() + child.propagateEffectiveUpdate() +} + +// updateEffectiveAuthorizedRegions recomputes effective regions for the distributor. +func (d *Distributor) updateEffectiveAuthorizedRegions() { + effective := make(map[string]*region.Region) + if len(d.ParentDistributors) == 0 { + effective = copyMap(d.AuthorizedRegions) + } else { + for _, r := range d.AuthorizedRegions { + if allowedByAllParents(r, d.ParentDistributors) { + effective[r.FullPath] = r + } + } + } + effective = subtract(effective, d.ExcludedRegions) + d.EffectiveAuthorizedRegions = effective +} + +// propagateEffectiveUpdate recursively updates effective regions for all child distributors. +func (d *Distributor) propagateEffectiveUpdate() { + for _, child := range d.Children { + child.updateEffectiveAuthorizedRegions() + child.propagateEffectiveUpdate() + } +} + +func (d *Distributor) HasPermission(r *region.Region) bool { + // Check if r is excluded at this distributor. + for _, ex := range d.ExcludedRegions { + if isSubregion(r, ex) { + return false + } + } + // Check if r is allowed in the effective regions. + for _, er := range d.EffectiveAuthorizedRegions { + if isSubregion(r, er) { + return true + } + } + return false +} + +// isSubregion returns true if child is equal to or a subregion of parent. +// It splits the FullPath strings into tokens and checks that parent's tokens form a prefix of child's tokens. +func isSubregion(child, parent *region.Region) bool { + if parent == nil { + return false + } + if parent.FullPath == "" { + return true + } + childParts := strings.Split(child.FullPath, "-") + parentParts := strings.Split(parent.FullPath, "-") + if len(childParts) < len(parentParts) { + return false + } + for i, token := range parentParts { + if childParts[i] != token { + return false + } + } + return true +} + +func copyMap(src map[string]*region.Region) map[string]*region.Region { + newMap := make(map[string]*region.Region) + for k, v := range src { + newMap[k] = v + } + return newMap +} + +func subtract(a, b map[string]*region.Region) map[string]*region.Region { + result := copyMap(a) + for k := range b { + delete(result, k) + } + return result +} diff --git a/internal/models/region/region.go b/internal/models/region/region.go new file mode 100644 index 000000000..81b01ad00 --- /dev/null +++ b/internal/models/region/region.go @@ -0,0 +1,67 @@ +package region + +import ( + "strings" +) + +type Region struct { + Name string + Parent *Region + Children map[string]*Region + FullPath string // Pre-computed full path, e.g., "USA-California-LosAngeles" +} + +type RegionManager struct { + Root *Region + regionMap map[string]*Region +} + +func NewRegionManager() *RegionManager { + root := &Region{ + Name: "", + Children: make(map[string]*Region), + FullPath: "", // Root has an empty path + } + return &RegionManager{ + Root: root, + regionMap: make(map[string]*Region), + } +} + +// AddRegion adds a region given a full path (e.g., "USA-California-LosAngeles") +func (rm *RegionManager) AddRegion(path string) *Region { + parts := strings.Split(path, "-") + current := rm.Root + fullPath := "" + + for _, part := range parts { + if fullPath == "" { + fullPath = part + } else { + fullPath = fullPath + "-" + part + } + + if child, exists := current.Children[part]; exists { + current = child + } else { + newRegion := &Region{ + Name: part, + Parent: current, + Children: make(map[string]*Region), + FullPath: fullPath, + } + current.Children[part] = newRegion + current = newRegion + + rm.regionMap[fullPath] = newRegion + } + } + + return current +} + +// GetRegion retrieves a region by its full path in O(1) time i wanted this to be read optimized hence the ugly code :P +func (rm *RegionManager) GetRegion(path string) (*Region, bool) { + region, exists := rm.regionMap[path] + return region, exists +}