diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..723ef36 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.idea \ No newline at end of file diff --git a/cmd/problem1/main.go b/cmd/problem1/main.go new file mode 100644 index 0000000..1563bd6 --- /dev/null +++ b/cmd/problem1/main.go @@ -0,0 +1,11 @@ +package main + +import "github.com/challenge2019/delivery" + +func main() { + d := delivery.NewDeliveryService(&delivery.Repository{}) + _, err := d.FindMinCostPartners("./data/input.csv") + if err != nil { + panic(err) + } +} diff --git a/cmd/problem2/main.go b/cmd/problem2/main.go new file mode 100644 index 0000000..5f16642 --- /dev/null +++ b/cmd/problem2/main.go @@ -0,0 +1,11 @@ +package main + +import "github.com/challenge2019/delivery" + +func main() { + d := delivery.NewDeliveryService(&delivery.Repository{}) + _, err := d.Assign("./data/input.csv") + if err != nil { + panic(err) + } +} diff --git a/capacities.csv b/data/capacities.csv similarity index 61% rename from capacities.csv rename to data/capacities.csv index ae622cf..fe6943d 100644 --- a/capacities.csv +++ b/data/capacities.csv @@ -1,4 +1,4 @@ -"Partner ID","Capacity (in GB)" +partner_id, capacity P1 ,350 P2 ,500 P3 ,1500 diff --git a/input.csv b/data/input.csv similarity index 57% rename from input.csv rename to data/input.csv index 3f1c8fa..4131341 100644 --- a/input.csv +++ b/data/input.csv @@ -1,3 +1,4 @@ +delivery_id,amount,theatre_id D1,150,T1 D2,325,T2 D3,510,T1 diff --git a/data/output_problem1.csv b/data/output_problem1.csv new file mode 100644 index 0000000..8cd7808 --- /dev/null +++ b/data/output_problem1.csv @@ -0,0 +1,5 @@ +delivery_id,is_possible,partner_id,cost +D2,true,P3,3900 +D1,true,P3,3750 +D3,true,P3,15300 +D4,false,,0 diff --git a/data/output_problem2.csv b/data/output_problem2.csv new file mode 100644 index 0000000..361b1d0 --- /dev/null +++ b/data/output_problem2.csv @@ -0,0 +1,5 @@ +delivery_id,is_possible,partner_id,cost +D1,true,P1,2000 +D2,true,P2,3500 +D3,true,P3,15300 +D4,false,,0 diff --git a/partners.csv b/data/partners.csv similarity index 91% rename from partners.csv rename to data/partners.csv index 3ea8c59..e00d34e 100644 --- a/partners.csv +++ b/data/partners.csv @@ -1,4 +1,4 @@ -Theatre,Size Slab (in GB),Minimum cost,Cost Per GB,Partner ID +theatre_id,slab,min_cost,cost_gb,partner_id T1 ,0-100 ,1500 ,20 ,P1 T1 ,100-200 ,2000 ,13 ,P1 T1 ,200-300 ,2500 ,12 ,P1 diff --git a/delivery/entity.go b/delivery/entity.go new file mode 100644 index 0000000..6f20a4e --- /dev/null +++ b/delivery/entity.go @@ -0,0 +1,52 @@ +package delivery + +import ( + "strconv" + "strings" +) + +type Input struct { + DeliveryID TrimString `csv:"delivery_id"` + Amount int `csv:"amount"` + TheatreID TrimString `csv:"theatre_id"` +} + +type Output struct { + DeliveryID TrimString `csv:"delivery_id"` + IsPossible bool `csv:"is_possible"` + PartnerID TrimString `csv:"partner_id"` + Cost int `csv:"cost"` +} + +type Partner struct { + TheatreID TrimString `csv:"theatre_id"` + Slab Slab `csv:"slab"` + MinCost int `csv:"min_cost"` + CostPerGB int `csv:"cost_gb"` + PartnerID TrimString `csv:"partner_id"` +} + +type Slab struct { + MinSlab int + MaxSlab int +} + +type TrimString string + +func (s *TrimString) UnmarshalCSV(csv string) (err error) { + *s = TrimString(strings.TrimSpace(csv)) + return +} + +func (s *Slab) UnmarshalCSV(csv string) (err error) { + ss := strings.Split(strings.TrimSpace(csv), "-") + s.MinSlab, err = strconv.Atoi(ss[0]) + if err != nil { + return err + } + s.MaxSlab, err = strconv.Atoi(ss[1]) + if err != nil { + return err + } + return nil +} diff --git a/delivery/repository.go b/delivery/repository.go new file mode 100644 index 0000000..1059cca --- /dev/null +++ b/delivery/repository.go @@ -0,0 +1,25 @@ +package delivery + +import "github.com/challenge2019/file" + +type Repository struct{} + +func (r *Repository) FetchDeliveries(fileName string, deliveries *[]Input) chan error { + return file.ReadAsync(fileName, deliveries) +} + +func (r *Repository) FetchPartners(fileName string, partners *[]Partner) chan error { + return file.ReadAsync(fileName, partners) +} + +func (r *Repository) FetchCapacities(fileName string, capacities *map[TrimString]int) chan error { + return file.ReadToMapAsync(fileName, capacities) +} + +func (r *Repository) SaveDeliveriesOutput(fileName string, out *[]Output) error { + err := file.Write(fileName, out) + if err != nil { + return err + } + return nil +} diff --git a/delivery/service.go b/delivery/service.go new file mode 100644 index 0000000..aed8cbd --- /dev/null +++ b/delivery/service.go @@ -0,0 +1,213 @@ +package delivery + +import ( + "github.com/challenge2019/util" +) + +const ( + partnerFileName = "./data/partners.csv" + capacityFileName = "./data/capacities.csv" +) + +type Service struct { + Repo *Repository +} + +func NewDeliveryService(r *Repository) *Service { + return &Service{Repo: r} +} + +func (s *Service) checkPartner(in <-chan Partner, d Input) chan Output { + out := make(chan Output) + min := Output{DeliveryID: d.DeliveryID, IsPossible: false} + go func() { + for p := range in { + if d.TheatreID == p.TheatreID && d.Amount >= p.Slab.MinSlab && d.Amount <= p.Slab.MaxSlab { + cost := d.Amount * p.CostPerGB + if cost < p.MinCost { + cost = p.MinCost + } + min = Output{DeliveryID: d.DeliveryID, IsPossible: true, Cost: cost, PartnerID: p.PartnerID} + } + } + out <- min + close(out) + }() + + return out +} + +func (s *Service) FindMinCostPartners(input string) ([]Output, error) { + var ( + deliveries []Input + partners []Partner + ) + + errors := util.Merge( + s.Repo.FetchDeliveries(input, &deliveries), + s.Repo.FetchPartners(partnerFileName, &partners), + ) + + for err := range errors { + if err != nil { + return nil, err + } + } + + size := len(deliveries) + out := make([]chan Output, 0, size) + result := make([]Output, 0, size) + deliveryChannels := make([]chan Partner, size, size) + + for i := range deliveries { + c := make(chan Partner) + deliveryChannels[i] = c + out = append(out, s.checkPartner(c, deliveries[i])) + } + + util.FanOut(partners, deliveryChannels...) + + for o := range util.Merge(out...) { + result = append(result, o) + } + + err := s.Repo.SaveDeliveriesOutput("./data/output_problem1.csv", &result) + if err != nil { + return nil, err + } + + return result, nil +} + +func toMap(pp []Partner) map[TrimString][]Partner { + m := make(map[TrimString][]Partner) + + for _, p := range pp { + m[p.TheatreID] = append(m[p.TheatreID], p) + } + + return m +} + +type container struct { + value int + okCount int + deliveries []Output + capacityLeft map[TrimString]int +} + +func (c *container) checkOptimal(opt container) bool { + return c.okCount > opt.okCount || (c.okCount == opt.okCount && c.value <= opt.value && len(c.deliveries) >= len(opt.deliveries)) +} + +func (c *container) copyAndAdd(d Input, p Partner) container { + cl := copyMap(c.capacityLeft) + out := Output{DeliveryID: d.DeliveryID, IsPossible: false} + ok := c.okCount + value := c.value + + if cl[p.PartnerID] >= d.Amount && d.Amount >= p.Slab.MinSlab && d.Amount <= p.Slab.MaxSlab { + ok++ + cost := d.Amount * p.CostPerGB + if cost < p.MinCost { + cost = p.MinCost + } + value += cost + cl[p.PartnerID] = cl[p.PartnerID] - d.Amount + out = Output{DeliveryID: d.DeliveryID, IsPossible: true, Cost: cost, PartnerID: p.PartnerID} + } + + dd := make([]Output, len(c.deliveries), len(c.deliveries)+1) + copy(dd, c.deliveries) + + return container{ + value: value, + capacityLeft: cl, + okCount: ok, + deliveries: append(dd, out), + } +} + +func copyMap(m map[TrimString]int) map[TrimString]int { + cm := make(map[TrimString]int) + for k, v := range m { + cm[k] = v + } + return cm +} + +func newContainer(d Input, p Partner, cc map[TrimString]int) container { + var value int + cl := copyMap(cc) + out := Output{DeliveryID: d.DeliveryID, IsPossible: false} + ok := 0 + + if cl[p.PartnerID] >= d.Amount && d.Amount >= p.Slab.MinSlab && d.Amount <= p.Slab.MaxSlab { + ok++ + value = d.Amount * p.CostPerGB + if value < p.MinCost { + value = p.MinCost + } + cl[p.PartnerID] = cl[p.PartnerID] - d.Amount + out = Output{DeliveryID: d.DeliveryID, IsPossible: true, Cost: value, PartnerID: p.PartnerID} + } + + return container{ + value: value, + capacityLeft: cl, + okCount: ok, + deliveries: []Output{out}, + } +} + +func (s *Service) Assign(input string) ([]Output, error) { + var ( + deliveries []Input + partners []Partner + capacities map[TrimString]int + ) + + errors := util.Merge( + s.Repo.FetchDeliveries(input, &deliveries), + s.Repo.FetchPartners(partnerFileName, &partners), + s.Repo.FetchCapacities(capacityFileName, &capacities), + ) + + for err := range errors { + if err != nil { + return nil, err + } + } + + partnersMap := toMap(partners) + + var ( + containerSet []container + opt container + ) + + for _, d := range deliveries { + pp := partnersMap[d.TheatreID] + var cc []container + for _, p := range pp { + cc = append(cc, newContainer(d, p, capacities)) + + for i := 0; i < len(containerSet); i++ { + newC := containerSet[i].copyAndAdd(d, p) + cc = append(cc, newC) + + if newC.checkOptimal(opt) { + opt = newC + } + } + } + containerSet = append(containerSet, cc...) + } + + err := s.Repo.SaveDeliveriesOutput("./data/output_problem2.csv", &opt.deliveries) + if err != nil { + return nil, err + } + + return opt.deliveries, nil +} diff --git a/file/util.go b/file/util.go new file mode 100644 index 0000000..3415e0e --- /dev/null +++ b/file/util.go @@ -0,0 +1,74 @@ +package file + +import ( + "encoding/csv" + "github.com/gocarina/gocsv" + "os" +) + +func ReadAsync(fileName string, out interface{}) chan error { + c := make(chan error) + + go func() { + defer close(c) + + file, err := os.Open(fileName) + if err != nil { + c <- err + return + } + + defer file.Close() + + if err := gocsv.UnmarshalFile(file, out); err != nil { + c <- err + return + } + + c <- nil + }() + + return c +} + +func ReadToMapAsync(fileName string, out interface{}) chan error { + c := make(chan error) + + go func() { + defer close(c) + + file, err := os.Open(fileName) + if err != nil { + c <- err + return + } + + r := csv.NewReader(file) + + defer file.Close() + + if err := gocsv.UnmarshalCSVToMap(r, out); err != nil { + c <- err + return + } + + c <- nil + }() + + return c +} + +func Write(fileName string, data interface{}) (err error) { + err = os.Remove(fileName) + if err != nil && !os.IsNotExist(err) { + return + } + file, _ := os.Create(fileName) + defer file.Close() + + err = gocsv.MarshalFile(data, file) + if err != nil { + return + } + return +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..dda3591 --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module github.com/challenge2019 + +go 1.19 + +require github.com/gocarina/gocsv v0.0.0-20220927221512-ad3251f9fa25 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..2f542f9 --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +github.com/gocarina/gocsv v0.0.0-20220927221512-ad3251f9fa25 h1:wxgEEZvsnOTrDO2npSSKUMDx5IykfoGmro+/Vjc1BQ8= +github.com/gocarina/gocsv v0.0.0-20220927221512-ad3251f9fa25/go.mod h1:5YoVOkjYAQumqlV356Hj3xeYh4BdZuLE0/nRkf2NKkI= diff --git a/output1.csv b/output1.csv deleted file mode 100644 index 7c7b275..0000000 --- a/output1.csv +++ /dev/null @@ -1,4 +0,0 @@ -D1,true ,P1,2000 -D2,true ,P1,3250 -D3,true ,P3,15300 -D4,false,"","" diff --git a/output2.csv b/output2.csv deleted file mode 100644 index adcd15b..0000000 --- a/output2.csv +++ /dev/null @@ -1,4 +0,0 @@ -D1,true ,P2,3000 -D2,true ,P1,3250 -D3,true ,P3,15300 -D4,false,"","" diff --git a/util/util.go b/util/util.go new file mode 100644 index 0000000..64f0767 --- /dev/null +++ b/util/util.go @@ -0,0 +1,39 @@ +package util + +import "sync" + +func FanOut[T any](partners []T, out ...chan T) { + go func() { + for _, p := range partners { + for _, c := range out { + c <- p + } + } + + for _, c := range out { + close(c) + } + }() +} + +func Merge[T any](in ...chan T) chan T { + wg := sync.WaitGroup{} + out := make(chan T, len(in)) + wg.Add(len(in)) + + for _, c := range in { + go func(ch chan T) { + defer wg.Done() + for t := range ch { + out <- t + } + }(c) + } + + go func() { + wg.Wait() + close(out) + }() + + return out +}