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 { for i := range transactions { dupe := false for j := range anz.transactions { if transactions[i] == anz.transactions[j] { dupe = true } } if !dupe { anz.transactions = append(anz.transactions, transactions[i]) } } return len(transactions) } // 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{}} }