Lesson 3
Mastering Data Aggregation and Data Streams with Go: Building a Sales Records Aggregator
Introduction

Welcome to our lesson on mastering data aggregation and data streams with Go. In this lesson, you'll learn to build a basic sales records aggregator using Go's standard library and map data structures. We'll extend this functionality to handle more advanced operations such as filtering, data aggregation, and formatting. By the end of this lesson, you'll be proficient in managing and formatting data streams efficiently in Go.

Starter Task Methods and Their Definitions

To get started, we'll create a simple sales record aggregator in Go. Here are the functions we'll focus on:

  • func (s *SalesAggregator) AddSale(saleID string, amount float64, date time.Time) - Adds or updates a sale record with a unique identifier saleID, amount, and date.

  • func (s *SalesAggregator) GetSale(saleID string) (float64, bool) - Retrieves the sale amount associated with the saleID. Returns the amount and a bool indicating if the sale exists.

  • func (s *SalesAggregator) DeleteSale(saleID string) bool - Deletes the sale record with the given saleID. Returns true if the sale was deleted, false if it does not exist.

Are these functions clear so far? Great! Let's now look at how we would implement them.

Starter Task Implementation

Here is the complete code for the starter task:

Go
1package main 2 3import ( 4 "fmt" 5 "time" 6) 7 8type sale struct { 9 amount float64 10 date time.Time 11} 12 13type SalesAggregator struct { 14 sales map[string]sale 15} 16 17func (s *SalesAggregator) AddSale(saleID string, amount float64, date time.Time) { 18 s.sales[saleID] = sale{amount: amount, date: date} 19} 20 21func (s *SalesAggregator) GetSale(saleID string) (float64, bool) { 22 sale, exists := s.sales[saleID] 23 return sale.amount, exists 24} 25 26func (s *SalesAggregator) DeleteSale(saleID string) bool { 27 if _, exists := s.sales[saleID]; exists { 28 delete(s.sales, saleID) 29 return true 30 } 31 return false 32} 33 34func main() { 35 aggregator := SalesAggregator{sales: make(map[string]sale)} 36 37 // Add sales 38 aggregator.AddSale("001", 100.50, time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC)) 39 aggregator.AddSale("002", 200.75, time.Date(2023, 1, 15, 0, 0, 0, 0, time.UTC)) 40 41 // Get sale 42 if amount, found := aggregator.GetSale("001"); found { 43 fmt.Println(amount) // Output: 100.5 44 } else { 45 fmt.Println("Sale not found") 46 } 47 48 // Delete sale 49 fmt.Println(aggregator.DeleteSale("002")) // Output: true 50 if amount, found := aggregator.GetSale("002"); found { 51 fmt.Println(amount) 52 } else { 53 fmt.Println("Sale not found") // Output: Sale not found 54 } 55}

Let's quickly discuss this starter setup:

  • The sales map stores sale records with saleID as the key and a sale struct containing amount and date as the value.
  • AddSale adds a new sale or updates an existing sale ID.
  • GetSale retrieves the amount for a given sale ID and returns a bool to indicate if the sale exists.
  • DeleteSale removes the sale record for the given sale ID and returns a bool to indicate success.

Now that we have our basic aggregator, let's extend it to include more advanced functionalities.

New Methods and Their Definitions

To add complexity and usefulness to our sales aggregator, we'll introduce some additional functions for advanced data aggregation, filtering, and formatting functionalities.

  • func (s *SalesAggregator) AggregateSales(minAmount float64) (int, float64) - Returns the total number of sales and the total sales amount where the sale amount is above minAmount.

  • func (s *SalesAggregator) FormatSales(minAmount float64) string - Returns the sales data, filtered by minAmount, formatted as a plain text string. Includes sales statistics in the output.

  • func (s *SalesAggregator) GetSalesInDateRange(startDate, endDate time.Time) []SaleDetail - Retrieves all sales that occurred within the given date range, inclusive. Each sale includes saleID, amount, and date.

Let's implement these functions step by step.

Step 1: Implementing the 'Aggregate Sales' Method

We start by creating the AggregateSales function:

Go
1// ... previous code 2func (s *SalesAggregator) AggregateSales(minAmount float64) (int, float64) { 3 totalSales := 0 4 totalAmount := 0.0 5 for _, sale := range s.sales { 6 if sale.amount > minAmount { 7 totalSales++ 8 totalAmount += sale.amount 9 } 10 } 11 return totalSales, totalAmount 12} 13 14func main() { 15 aggregator := SalesAggregator{sales: make(map[string]sale)} 16 aggregator.AddSale("001", 100.50, time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC)) 17 aggregator.AddSale("002", 200.75, time.Date(2023, 1, 15, 0, 0, 0, 0, time.UTC)) 18 19 totalSales, totalAmount := aggregator.AggregateSales(50) 20 fmt.Printf("Total Sales: %d, Total Amount: %.2f\n", totalSales, totalAmount) 21 // Output: Total Sales: 2, Total Amount: 301.25 22}

This method simply iterates through the sales and sums those that exceed the minAmount.

Step 2: Implementing the 'Format Sales' Method

Next, we create the FormatSales function to output data in plain text format.

Go
1// ... previous code 2func (s *SalesAggregator) FormatSales(minAmount float64) string { 3 var builder strings.Builder 4 totalSales, totalAmount := s.AggregateSales(minAmount) 5 6 builder.WriteString("Sales:\n") 7 for saleID, sale := range s.sales { 8 if sale.amount > minAmount { 9 builder.WriteString(fmt.Sprintf("Sale ID: %s, Amount: %.2f, Date: %s\n", saleID, sale.amount, sale.date.Format("2006-01-02"))) 10 // date.Format("2006-01-02") formats date as "YYYY-MM-DD" 11 } 12 } 13 14 builder.WriteString(fmt.Sprintf("Summary:\nTotal Sales: %d, Total Amount: %.2f\n", totalSales, totalAmount)) 15 16 return builder.String() 17} 18 19func main() { 20 aggregator := SalesAggregator{sales: make(map[string]sale)} 21 aggregator.AddSale("001", 100.50, time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC)) 22 aggregator.AddSale("002", 200.75, time.Date(2023, 1, 15, 0, 0, 0, 0, time.UTC)) 23 24 fmt.Println(aggregator.FormatSales(50)) 25 // Output: 26 // Sales: 27 // Sale ID: 001, Amount: 100.50, Date: 2023-01-01 28 // Sale ID: 002, Amount: 200.75, Date: 2023-01-15 29 // Summary: 30 // Total Sales: 2, Total Amount: 301.25 31}

Let's analyze this code:

  • The FormatSales method constructs a formatted string of sales data, filtered by a specified minimum amount.
  • It calculates total sales and amount for sales above minAmount using AggregateSales.
  • A strings.Builder is used to build the output string efficiently.
  • The WriteString method is called on the builder to append sections of the string, including sales details and summary.
  • The method filters sales, writing sale ID, amount, and date formatted as "YYYY-MM-DD" to the builder for qualifying sales.
  • Finally, a summary line is appended, and the constructed string is returned.
Step 3: Implementing the 'Get Sales in Date Range' Method

Now, let's finally implement GetSalesInDateRange, which relies on Go's time package to filter sales records.

Go
1// ... previous code 2 3type SaleDetail struct { 4 saleID string 5 amount float64 6 date time.Time 7} 8 9 10func (s *SalesAggregator) GetSalesInDateRange(startDate, endDate time.Time) []SaleDetail { 11 var result []SaleDetail 12 13 for saleID, sale := range s.sales { 14 if !sale.date.Before(startDate) && !sale.date.After(endDate) { 15 result = append(result, SaleDetail{saleID: saleID, amount: sale.amount, date: sale.date}) 16 } 17 } 18 return result 19} 20 21func main() { 22 aggregator := SalesAggregator{sales: make(map[string]sale)} 23 aggregator.AddSale("001", 100.50, time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC)) 24 aggregator.AddSale("002", 200.75, time.Date(2023, 1, 15, 0, 0, 0, 0, time.UTC)) 25 26 salesInRange := aggregator.GetSalesInDateRange(time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC), time.Date(2023, 12, 31, 0, 0, 0, 0, time.UTC)) 27 for _, sale := range salesInRange { 28 fmt.Printf("Sale ID: %s, Amount: %.2f, Date: %s\n", sale.saleID, sale.amount, sale.date.Format("2006-01-02")) 29 } 30 // Output: 31 // Sale ID: 001, Amount: 100.50, Date: 2023-01-01 32 // Sale ID: 002, Amount: 200.75, Date: 2023-01-15 33}

In this code:

  • The GetSalesInDateRange method filters sales records within a specified date range using the time package.
  • It accepts startDate and endDate, both of type time.Time, which define the inclusive date range.
  • An empty slice of SaleDetail structs is initialized to store matching sales records.
  • The method iterates over the sales map and uses the time.Time methods Before and After to check if each sale's date falls within the range.
  • Sale records within the range are appended to the result slice as SaleDetail structs, which is ultimately returned.
Lesson Summary

Congratulations! You've extended a basic sales aggregator in Go to an advanced aggregator capable of filtering, aggregating, and formatting data using custom structs and Go's built-in features. These skills are pivotal in efficiently managing data streams, especially with large datasets in Go applications. Feel free to experiment with similar challenges to deepen your understanding. Well done!

Enjoy this lesson? Now it's time to practice with Cosmo!
Practice is how you turn knowledge into actual skills.