add prompt dir

main
bel 2023-10-15 10:42:45 -06:00
parent 1c06e472c2
commit 7e9b85d0f4
9 changed files with 3075 additions and 0 deletions

View File

@ -0,0 +1,47 @@
# Welcome to the Digits Go Work Sample!
At Digits, we care deeply about who joins our team, so congrats on making it to this round in the process!
One of our goals as we continue the conversation is to learn more about how you think about and solve technical problems, so that we can get a sense of what it will be like working together day-to-day. That's what we're going to focus on here.
**First things first:** If you have prior experience with go, please **only** spend 3 hours on this. If you are new to go, please allow yourself 4 hours.
We've designed it so there is more work here than it's possible for anyone to finish in 3 (or 4) hours, so don't worry about it. We're looking for *quality* of engineering, not *quantity* of engineering. Seriously :)
And, of course, feel free to use any resources you would in your normal day-to-day work experience: Google, StackOverflow, etc.
Please take the time to format your code using [gofmt](https://pkg.go.dev/cmd/gofmt) (trivial to do if you are using [VSCode](https://code.visualstudio.com/docs/languages/go#_formatting)). If you are new to go, [this](https://gobyexample.com/) is a great resource to get some pointers.
Without further ado, here's a quick intro to the basics...
<hr>
### Setting Up...
Make sure you have Go [installed](https://golang.org/doc/install). You'll need at least 1.13.
Then, head on over to `analyzer_test.go` where the instructions continue...
*When your 3 hours are up, follow the instructions at the bottom of this README to submit your work sample, along with a few written-response questions. Happy coding!!*
<hr>
## If You're Totally Stuck :-(
If the instructions above don't work for any reason, or you get hopelessly stuck while coding, don't stress! Send us a note at <hello@digits.com> and we'll help get you sorted!
## When Your 3 Hours Are Up :-)
Pencils down! We know how tempting it is to keep going, so we really appreciate you sticking to the time limit :)
To submit your work sample, Zip up this directory (everything except the .build sub-directory) and email it back to us along with your written responses to these questions:
1. How would you evaluate your work? What went well? What would you do differently?
1. What was the biggest insight you gained, or thing you learned, while working on this?
1. If you were to add another test challenge to this, what would it be? Why?
Thanks again for taking the time to complete this, and we're really looking forward to continuing the conversation!
**Happy coding!!**
—Team Digits

View File

@ -0,0 +1,18 @@
package analyzer
import "math"
type Amount float64
// Rounded rounds a float to the number of places specified.
func (amount Amount) Rounded(places int) Amount {
p := math.Pow10(places)
return Amount(math.Round(float64(amount)*p) / p)
}
// FormatUSD formats Amount as a string in US dollars.
func (amount Amount) FormatUSD() string {
// TODO: Not implemented.
// Hint: Number localization gets tricky. Can you find a library to help?
return ""
}

View File

@ -0,0 +1,89 @@
package analyzer
import (
"testing"
)
func TestRounded(t *testing.T) {
type args struct {
num Amount
places int
}
tests := []struct {
name string
args args
want Amount
}{
{
name: "zero",
args: args{num: 0.0, places: 2},
want: 0.0,
},
{
name: "negative",
args: args{num: -30.1, places: 2},
want: -30.1,
},
{
name: "long",
args: args{num: 500.123456789, places: 3},
want: 500.123,
},
{
name: "short",
args: args{num: 500.523456789, places: 1},
want: 500.5,
},
{
name: "money",
args: args{num: 3.50, places: 2},
want: 3.50,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := tt.args.num.Rounded(tt.args.places); got != tt.want {
t.Errorf("Rounded() = %v, want %v", got, tt.want)
}
})
}
}
func TestFormatUSD(t *testing.T) {
type args struct {
dollars Amount
}
tests := []struct {
name string
args args
want string
}{
{
name: "zero",
args: args{dollars: 0.0},
want: "$0.00",
},
{
name: "positive",
args: args{dollars: 13.35423},
want: "$13.35",
},
{
name: "negative",
args: args{dollars: -28.44},
want: "-$28.44",
},
{
name: "large",
args: args{dollars: 56000.03},
want: "$56,000.03",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := tt.args.dollars.FormatUSD(); got != tt.want {
t.Errorf("FormatUSD() = %q, want %q", got, tt.want)
}
})
}
}

View File

@ -0,0 +1,72 @@
package analyzer
import "errors"
// 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 {
anz.transactions = append(anz.transactions, transactions...)
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) {
// TODO: Not implemented.
return Transaction{}, errors.New("not implemented")
}
// 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 {
// TODO: Not implemented.
return nil
}
// 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 {
report := `
Digits Big Spenders Report
------------------------------------------------------
Name Amount
------------------------------------------------------`
// TODO: Not implemented.
// TODO: As a reminder, we're looking for well-factored, well-organized
// code. We encourage you to refactor, change, or add to any part
// of the non-test code as you see fit!
report += ""
return report
}
// New creates a new Analyzer with an empty set of transactions
func New() *Analyzer {
return &Analyzer{transactions: Transactions{}}
}

View File

@ -0,0 +1,279 @@
// At Digits, we care deeply about Test-Driven Development. If the build is "green",
// we ship it. We do not have a QA team, and we do not have an Ops team. Everyone is
// responsible for their code end-to-end, which means we're highly motivated to make it
// as stable and as secure as possible. We do this with automated tests.
//
// If you run `go test` on the command line, you'll see that we have some problems here.
// Only one test in the project passes!
//
// This is where you come in.
//
// We need you to go through the tests and help us fix them. Only spend 3 hours on this,
// and don't worry about fixing all of them. We've designed it so there is more work here
// than it is possible for anyone to finish in 3 hours, so don't worry about it. The tests
// build on each other, so they will be generally easier if you go in order, but feel free
// to jump around if you get stuck on one of them.
//
// Let's be clear: you are NOT being evaluated on how many tests you fix. We want to see
// thoughtful, easy-to-read, well-performing, secure, and well-documented code. At Digits,
// it's quality over quantity every day of the week.
//
// One last tip for you: You won't need to change any code in this file. Start by familiarizing
// yourself with the rest of the codebase, because that's where all your work will be.
//
// Best of luck, and we really appreciate all your help getting these tests to pass! Remember to
// stop after 3 hours wherever you are -- we know how tempting it is to keep going :-)
//
// -- Team Digits
package analyzer_test
import (
"fmt"
"testing"
analyzer "github.com/digits/WorkSample-Go"
)
// This work sample revolves around parsing and analyzing credit card transactions,
// which is something pretty core to what we work on at Digits!
//
// This is the Analyzer instance that the tests in this file will run against.
func newAnalyzer(t *testing.T) *analyzer.Analyzer {
txs, err := analyzer.TransactionsFromFile("testdata/transactions.json")
if err != nil {
t.Fatal(err)
}
anz := analyzer.New()
anz.Add(txs)
return anz
}
// The SampleTransactionData contains 188 distinct transactions.
//
// Let's validate that the analyzer sees all of them.
//
// - Note: This test should already be working correctly. No need to do anything here.
func TestAnalyzer_NewFromFile(t *testing.T) {
anz := newAnalyzer(t)
if n := anz.TransactionCount(); n != 188 {
t.Errorf("expected 188 transactions, got %d", n)
}
}
// Alright, your work starts here!
//
// The SampleTransactionData contains $48,501.17 worth of transactions, but something
// here isn't working. Let's make sure `Transactions.Sum()` is implemented correctly.
func TestAnalyzer_TransctionsSum(t *testing.T) {
anz := newAnalyzer(t)
if amount := anz.TransactionsAmount(); amount.Rounded(2) != 48_501.17 {
t.Errorf("expected 48,501.17 in transactions, got %f", amount)
}
}
// Next, let's find the largest transaction. Hmm, this one doesn't seem to be working either.
func TestAnalyzer_LargestTransaction(t *testing.T) {
anz := newAnalyzer(t)
trn, err := anz.LargestTransaction()
if err != nil {
t.Errorf("expected nil got %v", err)
}
if trn.Amount != 3_831.2 {
t.Errorf("expected largest transaction amount of $3,831.2, got %f", trn.Amount)
}
}
// Our analysis won't be accurate unless we make sure to de-duplicate transactions.
//
// Revamp the way we instantiate our Analyzer so that duplicate Transactions are discarded.
// What data structure can you employ to make this easy?
//
// - Note: Once you get this working, double check that the previous tests still pass!
func TestAnalyzer_DuplicatesExcluded(t *testing.T) {
anz := newAnalyzer(t)
txs, err := analyzer.TransactionsFromFile("testdata/transactions.json")
if err != nil {
t.Fatal(err)
}
// Re-adding the existing transactions should be a no-op.
anz.Add(txs)
if n := anz.TransactionCount(); n != 188 {
t.Errorf("expected 188 transactions, got %d", n)
}
}
// Now that we know our data has no duplicates, we can get on to some real analysis.
//
// Let's group the transactions by category to see how often we shopped at Hardware Stores,
// and how much we spent on Electrical Contractors.
func TestAnalyzer_GroupByCategory(t *testing.T) {
anz := newAnalyzer(t)
categories := anz.ByCategory()
if trns := categories["HARDWARE STORES"]; len(trns) != 12 {
t.Errorf("expected category 'HARDWARE STORES' to have 12 transactions, got %d", len(trns))
}
if trns := categories["ELECTRICAL CONTRACTORS"]; len(trns) != 1 {
t.Errorf("expected category 'ELECTRICAL CONTRACTORS' to have 1 transactions, got %d", len(trns))
}
if trns := categories["ELECTRICAL CONTRACTORS"]; trns.Sum() != 89.0 {
t.Errorf("expected category 'ELECTRICAL CONTRACTORS' to have a total amount of $89.00, got %f", trns.Sum())
}
}
// Let's take a break from Transactions, and start preparing our printed reports.
//
// To do this, we're going to need to be able to attractively print dollar amounts.
//
// Hop over to `amount_test.go` and get those tests straightened out before continuing.
//
// With currencies looking sharp, it's time we're able to print whole trasnactions.
//
// Take a look at `transaction_test.go` and make sure we're properly formatting transactions!
// Finally, we're ready for our first report! We'll define "big spenders" as people who,
// in aggregate across their transactions, have spent more than 2 standard deviations above
// the mean spender.
//
// - Note: No hints this time. You know what to do! :)
func TestAnalyzer_BigSpendersReport(t *testing.T) {
anz := newAnalyzer(t)
const sampleReport = `
Digits Big Spenders Report
------------------------------------------------------
Name Amount
------------------------------------------------------
R. Bowers $5,877.00
T. Munday $4,556.60
W. Edwards $3,248.79
P. Wood $2,888.36
------------------------------------------------------`
report := anz.BigSpenderReport()
if report != sampleReport {
t.Errorf("expected big spender report of:\n%s\n\nGot:\n%s", sampleReport, report)
}
}
// Playing with 188 hard-coded transactions is fun but not very interesting.
// Now, it's time to load transactions from the network, ~1000 at a time!
//
// - Warning: You will need an active network connection to complete the next few challenges.
func TestAnalyzer_TransactionsFromURLs(t *testing.T) {
const transactionsURL = "https://assets.digits.com/uploads/hiring/swift-work-sample/transactions-aa.json"
txs, err := analyzer.TransactionsFromURLs(transactionsURL)
if err != nil {
t.Errorf("failed adding transactions from URL (%s): %s", transactionsURL, err)
}
anz := newAnalyzer(t)
if n := anz.Add(txs); n != 980 {
t.Errorf("expected 980 new transactions, got %d", n)
}
if n := anz.TransactionCount(); n != 1168 {
t.Errorf("expected 1168 transactions, got %d", n)
}
const sampleReport = `
Digits Big Spenders Report
------------------------------------------------------
Name Amount
------------------------------------------------------
J. Heusel $17,832.64
G. Hines $14,787.33
T. Munday $12,967.93
M. Al-Harake $7,454.14
R. Bowers $6,368.09
P. Lloyd $5,986.06
M. Tornakian $5,395.37
C. Miller $5,025.00
B. Ritthaler $4,897.54
J. Potts $4,800.00
J. Teel $4,685.82
K. Baum $4,533.40
K. Boyd $4,488.84
A. Hunt $4,424.64
A. Kindred $4,329.10
V. Yarbrough-Tessman $4,077.30
G. Deaver $3,835.00
------------------------------------------------------`
report := anz.BigSpenderReport()
if report != sampleReport {
t.Errorf("expected big spender report of:\n%s\n\nGot:\n%s", sampleReport, report)
}
}
// Now that you have one file working, it's time to load Transactions from 10 at once!
//
// For this challenge, dive into channels, goroutines, and sync.WaitGroup to perform the
// requests in parallel.
func TestAnalyzer_TransactionsFromURLsConcurrent(t *testing.T) {
urls := []string{"aa", "ab", "ac", "ad", "ae", "af", "ag", "ah", "ai", "aj"}
for i, suffix := range urls {
urls[i] = fmt.Sprintf("https://assets.digits.com/uploads/hiring/swift-work-sample/transactions-%s.json", suffix)
}
txs, err := analyzer.TransactionsFromURLs(urls...)
if err != nil {
t.Errorf("failed adding transactions from URL (%v): %s", urls, err)
}
anz := newAnalyzer(t)
if n := anz.Add(txs); n != 9661 {
t.Errorf("expected 9661 transactions, got %d", n)
}
if n := anz.TransactionCount(); n != 9849 {
t.Errorf("expected 9849 transactions, got %d", n)
}
const sampleReport = `
Digits Big Spenders Report
------------------------------------------------------
Name Amount
------------------------------------------------------
M. Tornakian $149,097.79
G. Hines $139,769.73
J. Heusel $108,557.26
T. Munday $66,767.24
R. Bowers $38,639.01
S. Fitzpatrick $32,320.23
V. Yarbrough-Tessman $29,655.08
A. Steanson $28,500.78
A. Kindred $24,623.06
M. Clark $24,132.51
J. Teel $23,729.02
S. Earls $22,238.02
A. Ropers $22,033.28
D. Turner $21,205.21
D. Connelly $20,458.27
D. Whitefield $20,102.42
S. Clawson $19,945.81
M. Al-Harake $19,510.29
------------------------------------------------------`
report := anz.BigSpenderReport()
if report != sampleReport {
t.Errorf("expected big spender report of:\n%s\n\nGot:\n%s", sampleReport, report)
}
}

View File

@ -0,0 +1,8 @@
module github.com/digits/WorkSample-Go
go 1.13
require (
github.com/stretchr/testify v1.4.0
golang.org/x/text v0.3.2
)

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,61 @@
package analyzer
import (
"encoding/json"
"errors"
"os"
)
// Transaction represents a transaction from our upstream source.
// URL: https://catalog.data.gov/dataset/purchase-card-pcard-fiscal-year-2014
type Transaction struct {
Category string `json:"MerchantCategory"`
Vendor string
Description string
AgencyName string
CardholderLastName string
TransactionDate string
Amount Amount `json:",string"`
YearMonth string
CardholderFirstInitial string
AgencyNumber string
PostedDate string
}
func (trn Transaction) String() string {
// TODO: Not implemented.
return ""
}
// Transactions represents a list of Transaction
type Transactions []Transaction
// Sum adds all transactions together.
func (trns Transactions) Sum() Amount {
// TODO: Not implemented.
return 0.0
}
// Count is the number of unique Transactions.
func (trns Transactions) Count() int {
return len(trns)
}
func TransactionsFromFile(path string) (Transactions, error) {
jsonFile, err := os.Open(path)
if err != nil {
return nil, err
}
defer jsonFile.Close()
var transactions Transactions
if err := json.NewDecoder(jsonFile).Decode(&transactions); err != nil {
return nil, err
}
return transactions, nil
}
func TransactionsFromURLs(url ...string) (Transactions, error) {
return nil, errors.New("not implemented")
}

View File

@ -0,0 +1,55 @@
package analyzer
import (
"testing"
)
// - Note: The JSON data contains the person's name who made the transaction.
// Let's pipe that through so it's available.
//
// - Note: Pay attention to how we want negative transactions handled.
func TestTransaction_String(t *testing.T) {
tests := []struct {
name string
trn Transaction
want string
}{
{
name: "MCCOLLOMS INC",
trn: Transaction{
CardholderFirstInitial: "R",
CardholderLastName: "Bowers",
Amount: 3831.20,
Vendor: "MCCOLLOMS INC",
},
want: "R. Bowers spent $3,831.20 at MCCOLLOMS INC",
},
{
name: "QUANTUM ELECTRIC INC",
trn: Transaction{
CardholderFirstInitial: "B",
CardholderLastName: "Adams",
Amount: 89.00,
Vendor: "QUANTUM ELECTRIC INC",
},
want: "B. Adams spent $89.00 at QUANTUM ELECTRIC INC",
},
{
name: "MARRIOTT 33716 NEW ORLEAN",
trn: Transaction{
CardholderFirstInitial: "J",
CardholderLastName: "Golliver",
Amount: -514.11,
Vendor: "MARRIOTT 33716 NEW ORLEAN",
},
want: "MARRIOTT 33716 NEW ORLEAN refunded J. Golliver $514.11",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := tt.trn.String(); got != tt.want {
t.Errorf("Transaction.String() = %q, want %q", got, tt.want)
}
})
}
}