Repository: vaskoz/dailycodingproblem-go (go)

Issue:
Day 369
Good morning! Here's your coding interview problem for today.

This problem was asked by Two Sigma.

You’re tracking stock price at a given instance of time. Implement an API with the following functions: `add(), update(), remove()`, which adds/updates/removes a datapoint for the stock price you are tracking. The data is given as `(timestamp, price)`, where `timestamp` is specified in **unix epoch time**.

Also, provide `max(), min(), and average()` functions that give the max/min/average of all values seen thus far.

Pull request:
Day369
Resolves #745.

Patch:
diff --git a/README.md b/README.md
index a695f84..89e7ddc 100644
--- a/README.md
+++ b/README.md
@@ -364,4 +364,5 @@ problems from
 * I thought it was done at day 365. Apparently not.
 * [Day 367](https://github.com/vaskoz/dailycodingproblem-go/issues/740)
 * [Day 368](https://github.com/vaskoz/dailycodingproblem-go/issues/743)
+* [Day 369](https://github.com/vaskoz/dailycodingproblem-go/issues/745)
 
diff --git a/day369/problem.go b/day369/problem.go
new file mode 100644
index 0000000..409a538
--- /dev/null
+++ b/day369/problem.go
@@ -0,0 +1,113 @@
+package day369
+
+import "math"
+
+// Price is a stock price represented as float64.
+type Price float64
+
+// Timestamp is a Unix timestamp.
+type Timestamp uint
+
+// Datapoint is a timestamp and a price.
+type Datapoint struct {
+	Timestamp Timestamp
+	Price     Price
+}
+
+// StockService is the defined API for stock data points.
+type StockService interface {
+	AddOrUpdate(Datapoint)
+	Remove(Timestamp)
+
+	Max() Price
+	Min() Price
+	Avg() Price
+}
+
+type stockService struct {
+	max, min, avg Price
+	n             uint
+	data          map[Timestamp]Datapoint
+}
+
+func (ss *stockService) AddOrUpdate(d Datapoint) {
+	total := ss.avg * Price(ss.n)
+
+	if prev, exists := ss.data[d.Timestamp]; exists {
+		ss.data[d.Timestamp] = d
+		ss.avg = (total + d.Price - prev.Price) / Price(ss.n)
+
+		if d.Price < ss.min || d.Price > ss.max {
+			ss.min, ss.max = ss.recalculateMinMax()
+		}
+	} else {
+		if d.Price > ss.max {
+			ss.max = d.Price
+		}
+		if d.Price < ss.min {
+			ss.min = d.Price
+		}
+		ss.n++
+		ss.avg = (total + d.Price) / Price(ss.n)
+		ss.data[d.Timestamp] = d
+	}
+}
+
+func (ss *stockService) Remove(t Timestamp) {
+	if prev, exists := ss.data[t]; exists {
+		delete(ss.data, t)
+		total := ss.avg * Price(ss.n)
+		ss.n--
+
+		if ss.n == 0 {
+			ss.min = math.MaxFloat64
+			ss.max = -math.MaxFloat64
+			ss.avg = 0
+
+			return
+		}
+
+		ss.avg = (total - prev.Price) / Price(ss.n)
+
+		if prev.Price == ss.min || prev.Price == ss.max {
+			ss.min, ss.max = ss.recalculateMinMax()
+		}
+	}
+}
+
+func (ss *stockService) recalculateMinMax() (Price, Price) {
+	max := Price(0)
+	min := Price(math.MaxFloat64)
+
+	for _, dp := range ss.data {
+		if dp.Price < min {
+			min = dp.Price
+		}
+
+		if dp.Price > max {
+			max = dp.Price
+		}
+	}
+
+	return min, max
+}
+
+func (ss *stockService) Max() Price {
+	return ss.max
+}
+
+func (ss *stockService) Min() Price {
+	return ss.min
+}
+
+func (ss *stockService) Avg() Price {
+	return ss.avg
+}
+
+func NewStockService() StockService {
+	return &stockService{
+		min:  math.MaxFloat64,
+		max:  -math.MaxFloat64,
+		data: make(map[Timestamp]Datapoint),
+	}
+}


Test patch:
diff --git a/day369/problem_test.go b/day369/problem_test.go
new file mode 100644
index 0000000..95b035c
--- /dev/null
+++ b/day369/problem_test.go
@@ -0,0 +1,67 @@
+package day369
+
+import (
+	"math"
+	"testing"
+)
+
+// nolint
+var testcases = []struct {
+	action        string
+	data          Datapoint
+	min, max, avg Price
+}{
+	{"addOrUpdate", Datapoint{10, 100.0}, 100.0, 100.0, 100.0},
+	{"addOrUpdate", Datapoint{20, 500.0}, 100.0, 500.0, 300.0},
+	{"addOrUpdate", Datapoint{10, 1000.0}, 500.0, 1000.0, 750.0},
+	{"remove", Datapoint{20, 500.0}, 1000.0, 1000.0, 1000.0},
+	{"remove", Datapoint{10, 1000.0}, math.MaxFloat64, -math.MaxFloat64, 0},
+	{"addOrUpdate", Datapoint{100, 10000.0}, 10000.0, 10000.0, 10000.0},
+	{"addOrUpdate", Datapoint{200, 10000.0}, 10000.0, 10000.0, 10000.0},
+}
+
+func TestStockService(t *testing.T) {
+	t.Parallel()
+
+	ss := NewStockService()
+
+	for _, tc := range testcases {
+		switch tc.action {
+		case "addOrUpdate":
+			ss.AddOrUpdate(tc.data)
+		case "remove":
+			ss.Remove(tc.data.Timestamp)
+		}
+
+		if min := ss.Min(); min != tc.min {
+			t.Errorf("Expected min %v, got %v", tc.min, min)
+		}
+
+		if max := ss.Max(); max != tc.max {
+			t.Errorf("Expected max %v, got %v", tc.max, max)
+		}
+
+		if avg := ss.Avg(); avg != tc.avg {
+			t.Errorf("Expected avg %v, got %v", tc.avg, avg)
+		}
+	}
+}
+
+func BenchmarkStockService(b *testing.B) {
+	ss := NewStockService()
+
+	for i := 0; i < b.N; i++ {
+		for _, tc := range testcases {
+			switch tc.action {
+			case "addOrUpdate":
+				ss.AddOrUpdate(tc.data)
+			case "remove":
+				ss.Remove(tc.data.Timestamp)
+			}
+
+			ss.Min()
+			ss.Max()
+			ss.Avg()
+		}
+	}
+}