digits-2023-10-15/digits-work-sample-go-bree-.../analyzer.go

169 lines
5.3 KiB
Go

package analyzer
import (
"fmt"
"sort"
"gonum.org/v1/gonum/stat"
)
// Analyzer processes Transactions and generates reports.
type Analyzer struct {
// The list of Transactions available to analyze.
transactions Transactions
}
// Add adds unique transactions to Analyzer.
func (anz *Analyzer) Add(transactions Transactions) int {
added := 0
for i := range transactions {
dupe := false
for j := range anz.transactions {
if transactions[i].equals(anz.transactions[j]) {
dupe = true
break
}
}
if !dupe {
added += 1
anz.transactions = append(anz.transactions, transactions[i])
}
}
return added
}
// TransactionCount is the number of unique Transactions.
func (anz *Analyzer) TransactionCount() int {
return len(anz.transactions)
}
// TransactionsAmount is the total amount of all the transactions.
func (anz *Analyzer) TransactionsAmount() Amount {
return anz.transactions.Sum()
}
// LargestTransaction is the single largest transaction, ranked by
// the absolute value of the transaction amount. Returns an error if there
// are no transactions.
//
// - Note: Large negative transactions are still large--it just means the
// money moved the other direction.
func (anz *Analyzer) LargestTransaction() (Transaction, error) {
if anz == nil || len(anz.transactions) == 0 {
return Transaction{}, ErrNoTransactionsToAnalyze
}
largestIdx := 0
for i := 1; i < len(anz.transactions); i++ {
if anz.transactions[largestIdx].Amount.AbsoluteValue() < anz.transactions[i].Amount.AbsoluteValue() {
largestIdx = i
}
}
return anz.transactions[largestIdx], nil
}
// byCardholder groups the transactions by cardholder and returns a map of
// cardholder name to the list of transactions that are part of that cardholder.
func (anz *Analyzer) byCardholder() map[string]Transactions {
result := map[string]Transactions{}
for i := range anz.transactions {
transactionsForCardholder := result[anz.transactions[i].stringifyCardholder()]
transactionsForCardholder = append(transactionsForCardholder, anz.transactions[i])
result[anz.transactions[i].stringifyCardholder()] = transactionsForCardholder
}
return result
}
// ByCategory groups the transactions by category and returns a map of
// category name to the list of transactions that are part of that category.
func (anz *Analyzer) ByCategory() map[string]Transactions {
result := map[string]Transactions{}
for i := range anz.transactions {
transactionsInCategory := result[anz.transactions[i].Category]
transactionsInCategory = append(transactionsInCategory, anz.transactions[i])
result[anz.transactions[i].Category] = transactionsInCategory
}
return result
}
type cardholderAndSum struct {
cardholder string
sum Amount
}
type cardholderAndSums []cardholderAndSum
func (cardholderAndSums cardholderAndSums) Len() int {
return len(cardholderAndSums)
}
func (cardholderAndSums cardholderAndSums) Less(i, j int) bool {
if cardholderAndSums[i].sum == cardholderAndSums[j].sum {
return cardholderAndSums[i].cardholder > cardholderAndSums[j].cardholder
}
return cardholderAndSums[i].sum > cardholderAndSums[j].sum
}
func (cardholderAndSums cardholderAndSums) Swap(i, j int) {
cardholderAndSums[i], cardholderAndSums[j] = cardholderAndSums[j], cardholderAndSums[i]
}
// BigSpenderReport generates our Big Spender Report.
//
// "Big spenders" are defined as those people who, in aggregate
// across their transactions, have spent more than 2 standard
// deviations above the mean spender.
//
// The method determines who they are and generates a well-formatted
// report to call them out.
func (anz *Analyzer) BigSpenderReport() string {
if len(anz.transactions) == 0 {
return anz.formatCardholderAmountsAsUSD("Digits Big Spenders Report", nil)
}
meanSpend, stddevSpend := anz.spendMeanStdDevPerCardholder()
cardholderAndSums := make(cardholderAndSums, 0)
for cardholder, transactionsForCardholder := range anz.byCardholder() {
cardholderSpendSum := transactionsForCardholder.Sum()
if cardholderSpendSum-2*stddevSpend > meanSpend {
cardholderAndSums = append(cardholderAndSums, cardholderAndSum{cardholder: cardholder, sum: cardholderSpendSum})
}
}
sort.Slice(cardholderAndSums, cardholderAndSums.Less)
return anz.formatCardholderAmountsAsUSD("Digits Big Spenders Report", cardholderAndSums)
}
func (anz *Analyzer) spendMeanStdDevPerCardholder() (mean, stdddev Amount) {
spendPerCardholder := anz.spendPerCardholder()
meanf64, stdf64 := stat.MeanStdDev(spendPerCardholder, nil)
return Amount(meanf64), Amount(stdf64)
}
func (anz *Analyzer) spendPerCardholder() []float64 {
values := []float64{}
for _, transactions := range anz.byCardholder() {
values = append(values, float64(transactions.Sum()))
}
return values
}
func (anz *Analyzer) formatCardholderAmountsAsUSD(title string, cardholderAndSums []cardholderAndSum) string {
report := fmt.Sprintf(`
%s
------------------------------------------------------
Name Amount
------------------------------------------------------`, title)
for i := range cardholderAndSums {
report = fmt.Sprintf("%s\n%-30s%s",
report,
cardholderAndSums[i].cardholder,
cardholderAndSums[i].sum.FormatUSD(),
)
}
return fmt.Sprintf("%s\n------------------------------------------------------", report)
}
// New creates a new Analyzer with an empty set of transactions
func New() *Analyzer {
return &Analyzer{transactions: Transactions{}}
}