diff --git a/interfaces/_legacy/01-variadic-funcs/main.go b/interfaces/_legacy/01-variadic-funcs/main.go new file mode 100644 index 0000000..6dad71d --- /dev/null +++ b/interfaces/_legacy/01-variadic-funcs/main.go @@ -0,0 +1,73 @@ +// For more tutorials: https://blog.learngoprogramming.com +// +// Copyright © 2018 Inanc Gumus +// Learn Go Programming Course +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ +// + +package main + +import "fmt" + +func main() { + nums := []int{2, 3, 7} + fmt.Printf("nums : %d\n", nums) + + n := avgNoVariadic(nums) + fmt.Printf("avgNoVariadic : %d\n", n) + + n = avg(2, 3, 7) + fmt.Printf("avg(2, 3, 7) : %d\n", n) + + n = avg(2, 3, 7, 8) + fmt.Printf("avg(2, 3, 7, 8) : %d\n", n) + + // use ... to pass a slice + n = avg(nums...) + fmt.Printf("avg(nums...) : %d\n", n) + + // uses the existing slice + double(nums...) + fmt.Printf("double(nums...) : %d\n", nums) + + // creates a new slice + double(4, 6, 14) + fmt.Printf("double(4, 6, 14): %d\n", nums) + + // creates a nil slice + fmt.Printf("\nmain.nums : %p\n", nums) + investigate("passes main.nums", nums...) + investigate("passes main.nums", nums...) + investigate("passes args", 4, 6, 14) + investigate("passes args", 4, 6, 14) + investigate("no args") +} + +func investigate(msg string, nums ...int) { + fmt.Printf("investigate.nums: %12p -> %s\n", nums, msg) + + if len(nums) > 0 { + fmt.Printf("\tfirst element: %d\n", nums[0]) + } +} + +func double(nums ...int) { + for i := range nums { + nums[i] *= 2 + } +} + +func avg(nums ...int) int { + return sum(nums) / len(nums) +} + +func avgNoVariadic(nums []int) int { + return sum(nums) / len(nums) +} + +func sum(nums []int) (total int) { + for _, n := range nums { + total += n + } + return +} diff --git a/interfaces/_legacy/02-func-values/main.go b/interfaces/_legacy/02-func-values/main.go new file mode 100644 index 0000000..e0f4ac4 --- /dev/null +++ b/interfaces/_legacy/02-func-values/main.go @@ -0,0 +1,52 @@ +// For more tutorials: https://blog.learngoprogramming.com +// +// Copyright © 2018 Inanc Gumus +// Learn Go Programming Course +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ +// + +package main + +import "fmt" + +type filterFunc func(int) bool + +func main() { + signatures() + funcValues() +} + +func isEven(n int) bool { + return n%2 == 0 +} + +func isOdd(m int) bool { + return m%2 == 1 +} + +func signatures() { + fmt.Println("••• FUNC SIGNATURES (TYPES) •••") + fmt.Printf("isEven : %T\n", isEven) + fmt.Printf("isOdd : %T\n", isOdd) +} + +func funcValues() { + fmt.Println("\n••• FUNC VALUES (VARS) •••") + + // var value func(int) bool + var value filterFunc + fmt.Printf("value nil? : %t\n", value == nil) + + value = isEven + fmt.Printf("value(2) : %t\n", value(2)) + fmt.Printf("isEven(2) : %t\n", isEven(2)) + + value = isOdd + fmt.Printf("value(1) : %t\n", value(1)) + fmt.Printf("isOdd(1) : %t\n", isOdd(1)) + + fmt.Printf("value nil? : %t\n", value == nil) + fmt.Printf("value : %p\n", value) + fmt.Printf("isEven : %p\n", isEven) + fmt.Printf("isOdd : %p\n", isOdd) +} diff --git a/interfaces/_legacy/03-func-to-func/main.go b/interfaces/_legacy/03-func-to-func/main.go new file mode 100644 index 0000000..d8102be --- /dev/null +++ b/interfaces/_legacy/03-func-to-func/main.go @@ -0,0 +1,82 @@ +// For more tutorials: https://blog.learngoprogramming.com +// +// Copyright © 2018 Inanc Gumus +// Learn Go Programming Course +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ +// + +package main + +import ( + "fmt" + "strings" + "unicode" +) + +type filterFunc func(int) bool + +func main() { + signatures() + + fmt.Println("\n••• WITHOUT FUNC VALUES •••") + nums := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10} + + fmt.Printf("evens : %d\n", filterEvens(nums...)) + fmt.Printf("odds : %d\n", filterOdds(nums...)) + + fmt.Println("\n••• FUNC VALUES •••") + fmt.Printf("evens : %d\n", filter(isEven, nums...)) + fmt.Printf("odds : %d\n", filter(isOdd, nums...)) + + fmt.Println("\n••• MAPPING •••") + fmt.Println(strings.Map(unpunct, "hello!!! HOW ARE YOU???? :))")) + fmt.Println(strings.Map(unpunct, "TIME IS UP!")) +} + +func unpunct(r rune) rune { + if unicode.IsPunct(r) { + return -1 + } + return unicode.ToLower(r) +} + +func filter(f filterFunc, nums ...int) (filtered []int) { + for _, n := range nums { + if f(n) { + filtered = append(filtered, n) + } + } + return +} + +func filterOdds(nums ...int) (filtered []int) { + for _, n := range nums { + if isOdd(n) { + filtered = append(filtered, n) + } + } + return +} + +func filterEvens(nums ...int) (filtered []int) { + for _, n := range nums { + if isEven(n) { + filtered = append(filtered, n) + } + } + return +} + +func isEven(n int) bool { + return n%2 == 0 +} + +func isOdd(m int) bool { + return m%2 == 1 +} + +func signatures() { + fmt.Println("••• FUNC SIGNATURES (TYPES) •••") + fmt.Printf("isEven : %T\n", isEven) + fmt.Printf("isOdd : %T\n", isOdd) +} diff --git a/interfaces/_legacy/04-closures/main.go b/interfaces/_legacy/04-closures/main.go new file mode 100644 index 0000000..9a62a75 --- /dev/null +++ b/interfaces/_legacy/04-closures/main.go @@ -0,0 +1,76 @@ +// For more tutorials: https://blog.learngoprogramming.com +// +// Copyright © 2018 Inanc Gumus +// Learn Go Programming Course +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ +// + +package main + +import "fmt" + +type filterFunc func(int) bool + +func main() { + nums := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10} + + fmt.Println("••• FUNC VALUES •••") + fmt.Printf("evens : %d\n", filter(isEven, nums...)) + fmt.Printf("odds : %d\n", filter(isOdd, nums...)) + fmt.Printf("> 3 : %d\n", filter(greaterThan3, nums...)) + fmt.Printf("> 6 : %d\n", filter(greaterThan6, nums...)) + + // ======================================================================== + + fmt.Println("\n••• CLOSURES •••") + + var min int + greaterThan := func(n int) bool { + return n > min + } + + min = 3 + fmt.Printf("> 3 : %d\n", filter(greaterThan, nums...)) + + min = 6 + fmt.Printf("> 6 : %d\n", filter(greaterThan, nums...)) + + // min = 1 + // fmt.Printf("> 1 : %d\n", filter(greaterThan, nums...)) + // min = 2 + // fmt.Printf("> 2 : %d\n", filter(greaterThan, nums...)) + // min = 3 + // fmt.Printf("> 3 : %d\n", filter(greaterThan, nums...)) + + var filterers []filterFunc + for i := 1; i <= 3; i++ { + current := i + + filterers = append(filterers, func(n int) bool { + min = current + return greaterThan(n) + }) + } + + printer(filterers, nums...) +} + +func printer(filterers []filterFunc, nums ...int) { + for i, filterer := range filterers { + fmt.Printf("> %d : %d\n", i+1, filter(filterer, nums...)) + } +} + +func greaterThan6(n int) bool { return n > 6 } +func greaterThan3(n int) bool { return n > 3 } +func isEven(n int) bool { return n%2 == 0 } +func isOdd(m int) bool { return m%2 == 1 } + +func filter(f filterFunc, nums ...int) (filtered []int) { + for _, n := range nums { + if f(n) { + filtered = append(filtered, n) + } + } + return +} diff --git a/interfaces/_legacy/05-higher-order-funcs/main.go b/interfaces/_legacy/05-higher-order-funcs/main.go new file mode 100644 index 0000000..ef5da4c --- /dev/null +++ b/interfaces/_legacy/05-higher-order-funcs/main.go @@ -0,0 +1,58 @@ +// For more tutorials: https://blog.learngoprogramming.com +// +// Copyright © 2018 Inanc Gumus +// Learn Go Programming Course +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ +// + +package main + +import ( + "fmt" +) + +type filterFunc func(int) bool + +func main() { + fmt.Println("••• HIGHER-ORDER FUNCS •••") + + nums := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10} + + odd := reverse(reverse(isEven)) + fmt.Printf("reversed : %t\n", odd(8)) + + fmt.Printf("> 3 : %d\n", filter(greater(3), nums...)) + fmt.Printf("> 6 : %d\n", filter(greater(6), nums...)) + fmt.Printf("<= 6 : %d\n", filter(lesseq(6), nums...)) + fmt.Printf("<= 6 : %d\n", filter(reverse(greater(6)), nums...)) +} + +func reverse(f filterFunc) filterFunc { + return func(n int) bool { + return !f(n) + } +} + +func greater(min int) filterFunc { + return func(n int) bool { + return n > min + } +} + +func lesseq(max int) filterFunc { + return func(n int) bool { + return n <= max + } +} + +func filter(f filterFunc, nums ...int) (filtered []int) { + for _, n := range nums { + if f(n) { + filtered = append(filtered, n) + } + } + return +} + +func isEven(n int) bool { return n%2 == 0 } +func isOdd(m int) bool { return m%2 == 1 } diff --git a/interfaces/_legacy/06-functional-programming/main.go b/interfaces/_legacy/06-functional-programming/main.go new file mode 100644 index 0000000..277e483 --- /dev/null +++ b/interfaces/_legacy/06-functional-programming/main.go @@ -0,0 +1,97 @@ +// For more tutorials: https://blog.learngoprogramming.com +// +// Copyright © 2018 Inanc Gumus +// Learn Go Programming Course +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ +// + +package main + +import ( + "fmt" +) + +type filterFunc func(int) bool + +func main() { + fmt.Println("••• HIGHER-ORDER FUNCS •••") + + nums := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10} + + odd := reverse(reverse(isEven)) + fmt.Printf("reversed : %t\n", odd(8)) + + fmt.Printf("> 3 : %d\n", filter(greater(3), nums...)) + fmt.Printf("> 6 : %d\n", filter(greater(6), nums...)) + fmt.Printf("<= 6 : %d\n", filter(lesseq(6), nums...)) + fmt.Printf("<= 6 : %d\n", filter(reverse(greater(6)), nums...)) + + fmt.Println("\n••• CLOSURES •••") + fmt.Printf("uniques : %d\n", filter(uniques(), 1, 1, 2, 3, 3)) + + dups := reverse(uniques()) + fmt.Printf("dups : %v\n", filter(dups, 1, 1, 2, 3, 3)) + + dups = reverse(uniques()) + fmt.Printf("dups dups : %v\n", filter(dups, 1, 1, 2, 3, 3, 3, 3)) + + dups = reverse(uniques()) + out := filter(dups, 1, 1, 2, 3, 3, 3, 3) + fmt.Printf("dups uniqs : %v\n", filter(uniques(), out...)) + + dups = reverse(uniques()) + chained := chain(reverse(greater(1)), dups, uniques()) + fmt.Printf("dups chain : %v\n", filter(chained, 1, 1, 2, 3, 3, 3, 3, 0, 0)) +} + +func chain(filters ...filterFunc) filterFunc { + return func(n int) bool { + for _, next := range filters { + if !next(n) { + return false + } + } + return true + } +} + +func uniques() filterFunc { + seen := make(map[int]bool) + + return func(n int) (ok bool) { + if !seen[n] { + seen[n], ok = true, true + } + return + } +} + +func reverse(f filterFunc) filterFunc { + return func(n int) bool { + return !f(n) + } +} + +func greater(min int) filterFunc { + return func(n int) bool { + return n > min + } +} + +func lesseq(max int) filterFunc { + return func(n int) bool { + return n <= max + } +} + +func filter(f filterFunc, nums ...int) (filtered []int) { + for _, n := range nums { + if f(n) { + filtered = append(filtered, n) + } + } + return +} + +func isEven(n int) bool { return n%2 == 0 } +func isOdd(m int) bool { return m%2 == 1 } diff --git a/interfaces/functional/Makefile b/interfaces/functional/Makefile new file mode 100644 index 0000000..b244980 --- /dev/null +++ b/interfaces/functional/Makefile @@ -0,0 +1,7 @@ +SHELL := /bin/bash + +r: + go run . < ../logs/log.txt + +t: + time go run . < ../logs/log.txt \ No newline at end of file diff --git a/interfaces/functional/chartwriter.go b/interfaces/functional/chartwriter.go new file mode 100644 index 0000000..d21a200 --- /dev/null +++ b/interfaces/functional/chartwriter.go @@ -0,0 +1,46 @@ +package main + +import ( + "io" + "sort" + "strconv" + + c "github.com/wcharczuk/go-chart" +) + +func chartWriter(w io.Writer) outputFn { + return func(res []result) error { + return chartWrite(w, res) + } +} + +func chartWrite(w io.Writer, res []result) error { + sort.Slice(res, func(i, j int) bool { + return res[i].domain > res[j].domain + }) + + donut := c.DonutChart{ + Title: "Total Visits Per Domain", + TitleStyle: c.Style{ + FontSize: 35, + Show: true, + FontColor: c.ColorAlternateGreen, + }, + Width: 1920, + Height: 800, + } + + for _, r := range res { + v := c.Value{ + Label: r.domain + r.page + ": " + strconv.Itoa(r.visits), + Value: float64(r.visits), + Style: c.Style{ + FontSize: 14, + }, + } + + donut.Values = append(donut.Values, v) + } + + return donut.Render(c.SVG, w) +} diff --git a/interfaces/functional/field.go b/interfaces/functional/field.go new file mode 100644 index 0000000..ad0d176 --- /dev/null +++ b/interfaces/functional/field.go @@ -0,0 +1,33 @@ +package main + +import ( + "errors" + "fmt" + "strconv" +) + +// field helps for field parsing +type field struct{ err error } + +// uatoi parses an unsigned integer string and saves the error. +// it assumes that the val is unsigned. +// for ease of usability: it returns an int instead of uint. +func (f *field) uatoi(name, val string) int { + n, err := strconv.Atoi(val) + if err != nil || n < 0 { + f.err = fmt.Errorf("incorrect field -> %q = %q", name, val) + } + return n +} + +func atoi(input []byte) (int, error) { + val := 0 + for i := 0; i < len(input); i++ { + char := input[i] + if char < '0' || char > '9' { + return 0, errors.New("invalid number") + } + val = val*10 + int(char) - '0' + } + return val, nil +} diff --git a/interfaces/functional/filters.go b/interfaces/functional/filters.go new file mode 100644 index 0000000..fe16049 --- /dev/null +++ b/interfaces/functional/filters.go @@ -0,0 +1,34 @@ +package main + +import "strings" + +func noopFilter(r result) bool { + return true +} + +func notUsing(filter filterFn) filterFn { + return func(r result) bool { + return !filter(r) + } +} + +func domainExtFilter(domains ...string) filterFn { + return func(r result) bool { + for _, domain := range domains { + if strings.HasSuffix(r.domain, "."+domain) { + return true + } + } + return false + } +} + +func domainFilter(domain string) filterFn { + return func(r result) bool { + return strings.Contains(r.domain, domain) + } +} + +func orgDomainsFilter(r result) bool { + return strings.HasSuffix(r.domain, ".org") +} diff --git a/interfaces/functional/groupers.go b/interfaces/functional/groupers.go new file mode 100644 index 0000000..d46ad56 --- /dev/null +++ b/interfaces/functional/groupers.go @@ -0,0 +1,20 @@ +package main + +// domainGrouper groups by domain. +// but it keeps the other fields. +// for example: it returns pages as well, but you shouldn't use them. +// exercise: write a function that erases superfluous data. +func domainGrouper(r result) string { + return r.domain +} + +func pageGrouper(r result) string { + return r.domain + r.page +} + +// groupBy allocates map unnecessarily +func noopGrouper(r result) string { + // with something like: + // return randomStrings() + return "" +} diff --git a/interfaces/functional/main.go b/interfaces/functional/main.go new file mode 100644 index 0000000..f26d4f5 --- /dev/null +++ b/interfaces/functional/main.go @@ -0,0 +1,44 @@ +// For more tutorials: https://blog.learngoprogramming.com +// +// Copyright © 2018 Inanc Gumus +// Learn Go Programming Course +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ +// + +package main + +import ( + "fmt" + "os" +) + +func main() { + p := pipeline{ + read: textReader(os.Stdin), + write: textWriter(os.Stdout), + filter: notUsing(domainExtFilter("io", "com")), + group: domainGrouper, + } + + // var p pipeline + // p. + // filterBy(notUsing(domainExtFilter("io", "com"))). + // groupBy(domainGrouper) + + if err := p.start(); err != nil { + fmt.Println("> Err:", err) + } +} + +// []outputter{textFile("results.txt"), chartFile("graph.png")} + +// func outputs(w io.Writer) outputFn { +// tw := textWriter(w) +// cw := chartWriter(w) + +// return func(rs []result) error { +// err := tw(rs) +// err = cw(rs) +// return err +// } +// } diff --git a/interfaces/functional/pipeline.go b/interfaces/functional/pipeline.go new file mode 100644 index 0000000..96ac4d3 --- /dev/null +++ b/interfaces/functional/pipeline.go @@ -0,0 +1,78 @@ +// For more tutorials: https://blog.learngoprogramming.com +// +// Copyright © 2018 Inanc Gumus +// Learn Go Programming Course +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ +// + +package main + +import "os" + +type ( + processFn func(r result) + inputFn func(processFn) error + outputFn func([]result) error + filterFn func(result) (include bool) + groupFn func(result) (key string) +) + +type pipeline struct { + read inputFn + write outputFn + filter filterFn + group groupFn +} + +func (p *pipeline) filterBy(f filterFn) *pipeline { p.filter = f; return p } +func (p *pipeline) groupBy(f groupFn) *pipeline { p.group = f; return p } +func (p *pipeline) from(f inputFn) *pipeline { p.read = f; return p } +func (p *pipeline) to(f outputFn) *pipeline { p.write = f; return p } + +func (p *pipeline) defaults() { + if p.filter == nil { + p.filter = noopFilter + } + + if p.group == nil { + p.group = domainGrouper + } + + if p.read == nil { + p.read = textReader(os.Stdin) + } + + if p.write == nil { + p.write = textWriter(os.Stdout) + } +} + +func (p *pipeline) start() error { + p.defaults() + + // retrieve and process the lines + sum := make(map[string]result) + + process := func(r result) { + if !p.filter(r) { + return + } + + k := p.group(r) + sum[k] = r.add(sum[k]) + } + + // return err from input reader + if err := p.read(process); err != nil { + return err + } + + // prepare the results for outputting + var out []result + for _, res := range sum { + out = append(out, res) + } + + // return err from output reader + return p.write(out) +} diff --git a/interfaces/functional/result.go b/interfaces/functional/result.go new file mode 100644 index 0000000..131cc7e --- /dev/null +++ b/interfaces/functional/result.go @@ -0,0 +1,83 @@ +package main + +import ( + "fmt" + "strings" +) + +const fieldsLength = 4 + +// result stores the parsed result for a domain +type result struct { + domain string + page string + visits int + uniques int +} + +// add adds the metrics of another result +func (r result) add(other result) result { + r.visits += other.visits + r.uniques += other.uniques + return r +} + +// parseFields parses and returns the parsing result +func parseFields(line string) (r result, err error) { + fields := strings.Fields(line) + + if len(fields) != fieldsLength { + return r, fmt.Errorf("wrong number of fields %q", fields) + } + + r.domain = fields[0] + r.page = fields[1] + + f := new(field) + r.visits = f.uatoi("visits", fields[2]) + r.uniques = f.uatoi("uniques", fields[3]) + + return r, f.err +} + +func fastParseFields(data []byte) (res result, err error) { + const separator = ' ' + + var findex int + + for i, j := 0, 0; i < len(data); i++ { + c := data[i] + last := len(data) == i+1 + + if c != separator && !last { + continue + } + + if last { + i = len(data) + } + + switch fval := data[j:i]; findex { + case 0: + res.domain = string(fval) + case 1: + res.page = string(fval) + case 2: + res.visits, err = atoi(fval) + case 3: + res.uniques, err = atoi(fval) + } + + if err != nil { + return res, err + } + + j = i + 1 + findex++ + } + + if findex != fieldsLength { + err = fmt.Errorf("wrong number of fields %q", data) + } + return res, err +} diff --git a/interfaces/functional/textreader.go b/interfaces/functional/textreader.go new file mode 100644 index 0000000..2f3e9a2 --- /dev/null +++ b/interfaces/functional/textreader.go @@ -0,0 +1,39 @@ +// For more tutorials: https://blog.learngoprogramming.com +// +// Copyright © 2018 Inanc Gumus +// Learn Go Programming Course +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ +// + +package main + +import ( + "bufio" + "fmt" + "io" +) + +func textReader(r io.Reader) inputFn { + return func(process processFn) error { + var ( + l = 1 + in = bufio.NewScanner(r) + ) + + for in.Scan() { + r, err := fastParseFields(in.Bytes()) + // r, err := parseFields(in.Text()) + if err != nil { + return fmt.Errorf("line %d: %v", l, err) + } + + process(r) + l++ + } + + if c, ok := r.(io.Closer); ok { + c.Close() + } + return in.Err() + } +} diff --git a/interfaces/functional/textwriter.go b/interfaces/functional/textwriter.go new file mode 100644 index 0000000..54886b2 --- /dev/null +++ b/interfaces/functional/textwriter.go @@ -0,0 +1,50 @@ +package main + +import ( + "fmt" + "io" + "sort" + "strings" +) + +// TODO: sort by result key interfaces section + +const ( + + // DOMAINS PAGES VISITS UNIQUES + // ^ ^ ^ ^ + // | | | | + header = "%-25s %-10s %10s %10s\n" + line = "%-25s %-10s %10d %10d\n" + footer = "\n%-36s %10d %10d\n" // -> "" VISITS UNIQUES + dash = "-" + dashLength = 58 +) + +func textWriter(w io.Writer) outputFn { + return func(res []result) error { + sort.Slice(res, func(i, j int) bool { + return res[i].domain > res[j].domain + }) + + var total result + + fmt.Fprintf(w, header, "DOMAINS", "PAGES", "VISITS", "UNIQUES") + fmt.Fprintf(w, strings.Repeat(dash, dashLength)+"\n") + + for _, r := range res { + total = total.add(r) + fmt.Fprintf(w, line, r.domain, r.page, r.visits, r.uniques) + } + + fmt.Fprintf(w, footer, "", total.visits, total.uniques) + + return nil + } +} + +func noWhere() outputFn { + return func(res []result) error { + return nil + } +} diff --git a/interfaces/logparser-testing/log.txt b/interfaces/logparser-testing/log.txt new file mode 100644 index 0000000..b449a83 --- /dev/null +++ b/interfaces/logparser-testing/log.txt @@ -0,0 +1,6 @@ +learngoprogramming.com 10 200 +learngoprogramming.com 10 300 +golang.org 4 50 +golang.org 6 100 +blog.golang.org 20 25 +blog.golang.org 10 1 \ No newline at end of file diff --git a/interfaces/logparser-testing/log_err_missing.txt b/interfaces/logparser-testing/log_err_missing.txt new file mode 100644 index 0000000..8fbe528 --- /dev/null +++ b/interfaces/logparser-testing/log_err_missing.txt @@ -0,0 +1,6 @@ +learngoprogramming.com 10 200 +learngoprogramming.com 10 +golang.org 4 50 +golang.org 6 100 +blog.golang.org 20 25 +blog.golang.org 10 1 \ No newline at end of file diff --git a/interfaces/logparser-testing/log_err_negative.txt b/interfaces/logparser-testing/log_err_negative.txt new file mode 100644 index 0000000..b099716 --- /dev/null +++ b/interfaces/logparser-testing/log_err_negative.txt @@ -0,0 +1,6 @@ +learngoprogramming.com 10 200 +learngoprogramming.com 10 300 +golang.org -100 50 +golang.org 6 100 +blog.golang.org 20 25 +blog.golang.org 10 1 \ No newline at end of file diff --git a/interfaces/logparser-testing/log_err_str.txt b/interfaces/logparser-testing/log_err_str.txt new file mode 100644 index 0000000..4ccb676 --- /dev/null +++ b/interfaces/logparser-testing/log_err_str.txt @@ -0,0 +1,6 @@ +learngoprogramming.com 10 200 +learngoprogramming.com 10 THREE-HUNDRED +golang.org FOUR 50 +golang.org 6 100 +blog.golang.org 20 25 +blog.golang.org 10 1 \ No newline at end of file diff --git a/interfaces/logparser-testing/main.go b/interfaces/logparser-testing/main.go new file mode 100644 index 0000000..a2372c6 --- /dev/null +++ b/interfaces/logparser-testing/main.go @@ -0,0 +1,26 @@ +// For more tutorials: https://blog.learngoprogramming.com +// +// Copyright © 2018 Inanc Gumus +// Learn Go Programming Course +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ +// + +package main + +import ( + "bufio" + "os" + + "github.com/inancgumus/learngo/27-interfaces/logparser-testing/report" +) + +func main() { + p := report.New() + + in := bufio.NewScanner(os.Stdin) + for in.Scan() { + p.Parse(in.Text()) + } + + summarize(p.Summarize(), p.Err(), in.Err()) +} diff --git a/interfaces/logparser-testing/main_test.go b/interfaces/logparser-testing/main_test.go new file mode 100644 index 0000000..b706f78 --- /dev/null +++ b/interfaces/logparser-testing/main_test.go @@ -0,0 +1,59 @@ +// +build integration + +// go test -tags=integration + +package main_test + +import ( + "bytes" + "os/exec" + "strings" + "testing" +) + +const ( + okIn = ` +a.com 1 2 +b.com 3 4 +a.com 4 5 +b.com 6 7` + + okOut = ` +DOMAIN VISITS TIME SPENT +----------------------------------------------------------------- +a.com 5 7 +b.com 9 11 + +TOTAL 14 18` +) + +func TestSummary(t *testing.T) { + tests := []struct { + name, in, out string + }{ + {"valid input", okIn, okOut}, + {"missing fields", "a.com 1 2\nb.com 3", "> Err: line #2: missing fields: [b.com 3]"}, + {"incorrect visits", "a.com 1 2\nb.com -1 1", `> Err: line #2: incorrect visits: "-1"`}, + {"incorrect time spent", "a.com 1 2\nb.com 3 -1", `> Err: line #2: incorrect time spent: "-1"`}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + run(t, strings.TrimSpace(tt.in), strings.TrimSpace(tt.out)) + }) + } +} + +func run(t *testing.T, in, out string) { + cmd := exec.Command("go", "run", ".") + cmd.Stdin = strings.NewReader(in) + + got, err := cmd.CombinedOutput() + if err != nil { + t.Fatal(err) + } + + if !bytes.Equal(got, []byte(out+"\n")) { + t.Fatalf("\nwant:\n%s\n\ngot:\n%s", out, got) + } +} diff --git a/interfaces/logparser-testing/report/parser.go b/interfaces/logparser-testing/report/parser.go new file mode 100644 index 0000000..59b12fe --- /dev/null +++ b/interfaces/logparser-testing/report/parser.go @@ -0,0 +1,52 @@ +// For more tutorials: https://blog.learngoprogramming.com +// +// Copyright © 2018 Inanc Gumus +// Learn Go Programming Course +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ +// + +package report + +import ( + "fmt" +) + +// Parser parses the log file and generates a summary report. +type Parser struct { + summary *Summary // summarizes the parsing results + lines int // number of parsed lines (for the error messages) + lerr error // the last error occurred +} + +// New returns a new parsing state. +func New() *Parser { + return &Parser{summary: newSummary()} +} + +// Parse parses a log line and adds it to the summary. +func (p *Parser) Parse(line string) { + // if there was an error do not continue + if p.lerr != nil { + return + } + + // chain the parser's error to the result's + res, err := parse(line) + if p.lines++; err != nil { + p.lerr = fmt.Errorf("line #%d: %s", p.lines, err) + return + } + + p.summary.update(res) +} + +// Summarize summarizes the parsing results. +// Only use it after the parsing is done. +func (p *Parser) Summarize() *Summary { + return p.summary +} + +// Err returns the last error encountered +func (p *Parser) Err() error { + return p.lerr +} diff --git a/interfaces/logparser-testing/report/parser_test.go b/interfaces/logparser-testing/report/parser_test.go new file mode 100644 index 0000000..b0a1dce --- /dev/null +++ b/interfaces/logparser-testing/report/parser_test.go @@ -0,0 +1,55 @@ +package report_test + +import ( + "strings" + "testing" + + "github.com/inancgumus/learngo/28-methods/logparser-testing/report" +) + +func newParser(lines string) *report.Parser { + p := report.New() + p.Parse(lines) + return p +} + +func TestParserLineErrs(t *testing.T) { + p := newParser("a.com 1 2") + p.Parse("b.com -1 -1") + + want := "#2" + err := p.Err().Error() + + if !strings.Contains(err, want) { + t.Errorf("want: %q; got: %q", want, err) + } +} + +func TestParserStopsOnErr(t *testing.T) { + p := newParser("a.com 10 20") + p.Parse("b.com -1 -1") + p.Parse("neverparses.com 30 40") + + s := p.Summarize() + if want, got := 10, s.Total().Visits; want != got { + t.Errorf("want: %d; got: %d", want, got) + } +} + +func TestParserIncorrectFields(t *testing.T) { + tests := []struct { + in, name string + }{ + {"a.com", "missing fields"}, + {"a.com -1 2", "incorrect visits"}, + {"a.com 1 -1", "incorrect time spent"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if p := newParser(tt.in); p.Err() == nil { + t.Errorf("in: %q; got: nil err", tt.in) + } + }) + } +} diff --git a/interfaces/logparser-testing/report/result.go b/interfaces/logparser-testing/report/result.go new file mode 100644 index 0000000..95e0022 --- /dev/null +++ b/interfaces/logparser-testing/report/result.go @@ -0,0 +1,60 @@ +// For more tutorials: https://blog.learngoprogramming.com +// +// Copyright © 2018 Inanc Gumus +// Learn Go Programming Course +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ +// + +package report + +import ( + "fmt" + "strconv" + "strings" +) + +// always put all the related things together as in here + +// Result stores metrics for a domain +// it uses the value mechanics, +// because it doesn't have to update anything +type Result struct { + Domain string `json:"domain"` + Visits int `json:"visits"` + TimeSpent int `json:"time_spent"` + // add more metrics if needed +} + +// add adds the metrics of another Result to itself and returns a new Result +func (r Result) add(other Result) Result { + return Result{ + Domain: r.Domain, + Visits: r.Visits + other.Visits, + TimeSpent: r.TimeSpent + other.TimeSpent, + } +} + +// parse parses a single log line +func parse(line string) (r Result, err error) { + fields := strings.Fields(line) + if len(fields) != 3 { + return r, fmt.Errorf("missing fields: %v", fields) + } + + f := new(field) + r.Domain = fields[0] + r.Visits = f.atoi("visits", fields[1]) + r.TimeSpent = f.atoi("time spent", fields[2]) + return r, f.err +} + +// field helps for field parsing +type field struct{ err error } + +func (f *field) atoi(name, val string) int { + n, err := strconv.Atoi(val) + if n < 0 || err != nil { + f.err = fmt.Errorf("incorrect %s: %q", name, val) + } + return n +} diff --git a/interfaces/logparser-testing/report/summary.go b/interfaces/logparser-testing/report/summary.go new file mode 100644 index 0000000..f1b88bd --- /dev/null +++ b/interfaces/logparser-testing/report/summary.go @@ -0,0 +1,86 @@ +// For more tutorials: https://blog.learngoprogramming.com +// +// Copyright © 2018 Inanc Gumus +// Learn Go Programming Course +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ +// + +package report + +import ( + "sort" +) + +// Summary aggregates the parsing results +type Summary struct { + sum map[string]Result // metrics per domain + domains []string // unique domain names + total Result // total visits for all domains +} + +// newSummary constructs and initializes a new summary +// You can't use its methods without pointer mechanics +func newSummary() *Summary { + return &Summary{sum: make(map[string]Result)} +} + +// Update updates the report for the given parsing result +func (s *Summary) update(r Result) { + domain := r.Domain + if _, ok := s.sum[domain]; !ok { + s.domains = append(s.domains, domain) + } + + // let the result handle the addition + // this allows us to manage the result in once place + // and this way it becomes easily extendable + s.total = s.total.add(r) + s.sum[domain] = r.add(s.sum[domain]) +} + +// Iterator returns `next()` to detect when the iteration ends, +// and a `cur()` to return the current result. +// iterator iterates sorted by domains. +func (s *Summary) Iterator() (next func() bool, cur func() Result) { + sort.Strings(s.domains) + + // remember the last iterated result + var last int + + next = func() bool { + defer func() { last++ }() + return len(s.domains) > last + } + + cur = func() Result { + // returns a copy so the caller cannot change it + name := s.domains[last-1] + return s.sum[name] + } + + return +} + +// Total returns the total metrics +func (s *Summary) Total() Result { + return s.total +} + +// For the interfaces section +// +// MarshalJSON marshals a report to JSON +// Alternative: unexported embedding +// func (s *Summary) MarshalJSON() ([]byte, error) { +// type total struct { +// *Result +// IgnoreDomain *string `json:"domain,omitempty"` +// } + +// return json.Marshal(struct { +// Sum map[string]Result `json:"summary"` +// Domains []string `json:"domains"` +// Total total `json:"total"` +// }{ +// Sum: s.sum, Domains: s.domains, Total: total{Result: &s.total}, +// }) +// } diff --git a/interfaces/logparser-testing/report/summary_test.go b/interfaces/logparser-testing/report/summary_test.go new file mode 100644 index 0000000..2a0fc50 --- /dev/null +++ b/interfaces/logparser-testing/report/summary_test.go @@ -0,0 +1,44 @@ +package report_test + +import ( + "testing" + + "github.com/inancgumus/learngo/28-methods/logparser-testing/report" +) + +func TestSummaryTotal(t *testing.T) { + p := newParser("a.com 1 2") + p.Parse("b.com 3 4") + + s := p.Summarize() + + want := report.Result{Domain: "", Visits: 4, TimeSpent: 6} + if got := s.Total(); want != got { + t.Errorf("want: %+v; got: %+v", want, got) + } +} + +func TestSummaryIterator(t *testing.T) { + p := newParser("a.com 1 2") + p.Parse("a.com 3 4") + p.Parse("b.com 5 6") + + s := p.Summarize() + next, cur := s.Iterator() + + wants := []report.Result{ + {Domain: "a.com", Visits: 4, TimeSpent: 6}, + {Domain: "b.com", Visits: 5, TimeSpent: 6}, + } + + for _, want := range wants { + t.Run(want.Domain, func(t *testing.T) { + if got := next(); !got { + t.Errorf("next(): want: %t; got: %t", true, got) + } + if got := cur(); want != got { + t.Errorf("cur(): want: %+v; got: %+v", want, got) + } + }) + } +} diff --git a/interfaces/logparser-testing/summarize.go b/interfaces/logparser-testing/summarize.go new file mode 100644 index 0000000..1573f4c --- /dev/null +++ b/interfaces/logparser-testing/summarize.go @@ -0,0 +1,73 @@ +// For more tutorials: https://blog.learngoprogramming.com +// +// Copyright © 2018 Inanc Gumus +// Learn Go Programming Course +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ +// + +package main + +import ( + "encoding/json" + "fmt" + "os" + "strings" + + "github.com/inancgumus/learngo/27-interfaces/logparser-testing/report" +) + +// summarize prints the parsing results. +// +// it prints the errors and returns if there are any. +// +// --json flag encodes to json and prints. +func summarize(sum *report.Summary, errors ...error) { + if errs(errors...) { + return + } + + if args := os.Args[1:]; len(args) == 1 && args[0] == "--json" { + encode(sum) + return + } + stdout(sum) +} + +// encodes the summary to json +func encode(sum *report.Summary) { + out, err := json.MarshalIndent(sum, "", "\t") + if err != nil { + panic(err) + } + os.Stdout.Write(out) +} + +// prints the summary to standard out +func stdout(sum *report.Summary) { + const ( + head = "%-30s %10s %20s\n" + val = "%-30s %10d %20d\n" + ) + + fmt.Printf(head, "DOMAIN", "VISITS", "TIME SPENT") + fmt.Println(strings.Repeat("-", 65)) + + for next, cur := sum.Iterator(); next(); { + r := cur() + fmt.Printf(val, r.Domain, r.Visits, r.TimeSpent) + } + + t := sum.Total() + fmt.Printf("\n"+val, "TOTAL", t.Visits, t.TimeSpent) +} + +// this variadic func simplifies the multiple error handling +func errs(errs ...error) (wasErr bool) { + for _, err := range errs { + if err != nil { + fmt.Printf("> Err: %s\n", err) + wasErr = true + } + } + return +} diff --git a/interfaces/logs/Makefile b/interfaces/logs/Makefile new file mode 100644 index 0000000..361a9bb --- /dev/null +++ b/interfaces/logs/Makefile @@ -0,0 +1,33 @@ +SHELL := /bin/bash +LINES = $$(wc -l log.txt | cut -f1 -d' ') +ECHO_LINES = echo -e ">> log.txt has $(LINES) lines" + +n ?= 18 +load: restore + @echo "enlarging the file with itself, please wait..." + + @for i in {1..$(n)}; do \ + awk 1 log.txt log.txt > log_.txt; \ + mv log_.txt log.txt; \ + rm -f log_.txt; \ + done + + @$(ECHO_LINES) + +restore: + @echo "restoring the file..." + # @git checkout log.txt + @$(ECHO_LINES) + +multiply: remove + @echo "creating 20 log files..." + @for i in {1..20}; do \ + echo log$${i}.txt; \ + cp log.txt log$${i}.txt; \ + done + +remove: + rm -f log{1..20}.txt + +lines: + @$(ECHO_LINES) \ No newline at end of file diff --git a/interfaces/logs/log.json b/interfaces/logs/log.json new file mode 100644 index 0000000..f930f01 --- /dev/null +++ b/interfaces/logs/log.json @@ -0,0 +1,18 @@ +[ + {"domain": "learngoprogramming.com", "page": "/", "visits": 10, "uniques": 5}, + {"domain": "learngoprogramming.com", "page": "/courses", "visits": 15, "uniques": 10}, + {"domain": "learngoprogramming.com", "page": "/courses", "visits": 10, "uniques": 5}, + {"domain": "learngoprogramming.com", "page": "/articles", "visits": 20, "uniques": 15}, + {"domain": "learngoprogramming.com", "page": "/articles", "visits": 5, "uniques": 2}, + {"domain": "golang.org", "page": "/", "visits": 40, "uniques": 20}, + {"domain": "golang.org", "page": "/", "visits": 20, "uniques": 10}, + {"domain": "golang.org", "page": "/blog", "visits": 45, "uniques": 25}, + {"domain": "golang.org", "page": "/blog", "visits": 15, "uniques": 5}, + {"domain": "blog.golang.org", "page": "/courses", "visits": 60, "uniques": 30}, + {"domain": "blog.golang.org", "page": "/courses", "visits": 30, "uniques": 20}, + {"domain": "blog.golang.org", "page": "/updates", "visits": 20, "uniques": 10}, + {"domain": "blog.golang.org", "page": "/reference", "visits": 65, "uniques": 35}, + {"domain": "blog.golang.org", "page": "/reference", "visits": 15, "uniques": 5}, + {"domain": "inanc.io", "page": "/about", "visits": 30, "uniques": 15}, + {"domain": "inanc.io", "page": "/about","visits": 70, "uniques": 35} +] \ No newline at end of file diff --git a/interfaces/logs/log.txt b/interfaces/logs/log.txt new file mode 100644 index 0000000..fa57104 --- /dev/null +++ b/interfaces/logs/log.txt @@ -0,0 +1,16 @@ +learngoprogramming.com / 10 5 +learngoprogramming.com /courses 15 10 +learngoprogramming.com /courses 10 5 +learngoprogramming.com /articles 20 15 +learngoprogramming.com /articles 5 2 +golang.org / 40 20 +golang.org / 20 10 +golang.org /blog 45 25 +golang.org /blog 15 5 +blog.golang.org /courses 60 30 +blog.golang.org /courses 30 20 +blog.golang.org /updates 20 10 +blog.golang.org /reference 65 35 +blog.golang.org /reference 15 5 +inanc.io /about 30 15 +inanc.io /about 70 35 \ No newline at end of file diff --git a/interfaces/logs/log_err_missing.txt b/interfaces/logs/log_err_missing.txt new file mode 100644 index 0000000..01f606f --- /dev/null +++ b/interfaces/logs/log_err_missing.txt @@ -0,0 +1,16 @@ +learngoprogramming.com / 10 5 +learngoprogramming.com /courses 15 10 +learngoprogramming.com /courses 10 5 +learngoprogramming.com /articles 20 15 +learngoprogramming.com /articles 5 2 +golang.org / 40 20 +golang.org / 20 10 +golang.org /blog 45 25 +golang.org /blog 15 5 +blog.golang.org /updates +blog.golang.org /updates 30 20 +blog.golang.org /updates 20 10 +blog.golang.org /reference 65 35 +blog.golang.org /reference 15 5 +inanc.io /about 30 15 +inanc.io /about 70 35 diff --git a/interfaces/logs/log_err_negative.txt b/interfaces/logs/log_err_negative.txt new file mode 100644 index 0000000..d84023e --- /dev/null +++ b/interfaces/logs/log_err_negative.txt @@ -0,0 +1,16 @@ +learngoprogramming.com / 10 5 +learngoprogramming.com /courses 15 10 +learngoprogramming.com /courses 10 5 +learngoprogramming.com /articles 20 15 +learngoprogramming.com /articles 5 2 +golang.org / 40 20 +golang.org / 20 10 +golang.org /blog 45 -250 +golang.org /blog 15 5 +blog.golang.org /updates 60 30 +blog.golang.org /updates 30 20 +blog.golang.org /updates 20 10 +blog.golang.org /reference 65 35 +blog.golang.org /reference 15 5 +inanc.io /about 30 15 +inanc.io /about 70 35 diff --git a/interfaces/logs/log_err_str.txt b/interfaces/logs/log_err_str.txt new file mode 100644 index 0000000..20272ab --- /dev/null +++ b/interfaces/logs/log_err_str.txt @@ -0,0 +1,16 @@ +learngoprogramming.com / 10 5 +learngoprogramming.com /courses 15 10 +learngoprogramming.com /courses 10 5 +learngoprogramming.com /articles 20 15 +learngoprogramming.com /articles 5 2 +golang.org / 40 TWENTY +golang.org / 20 10 +golang.org /blog 45 25 +golang.org /blog 15 5 +blog.golang.org /updates 60 30 +blog.golang.org /updates 30 20 +blog.golang.org /updates 20 10 +blog.golang.org /reference 65 35 +blog.golang.org /reference 15 5 +inanc.io /about 30 15 +inanc.io /about 70 35 \ No newline at end of file diff --git a/interfaces/notes.md b/interfaces/notes.md new file mode 100644 index 0000000..87f6a13 --- /dev/null +++ b/interfaces/notes.md @@ -0,0 +1,36 @@ +# Func Values + ++ Please feel free to put any function that match this signature here and it will be called each time my function is called. + ++ They are extremely useful when wanting to declare a block of code that you want to pass around. + ++ They allow functions to be passed as parameters. + ++ They can be chained together + +# Interfaces vs Func Values + ++ Use interfaces when a method needs to belong to an object (that needs to operate on the value of a type: io.Reader, sort.Interface, etc). + ++ Use func values when a function doesn't need to belong to an object. For simple, not-data oriented jobs: Filtering, splitting, mapping etc. + ++ An implementation of the interface means that the behaviour is intrinsic to the implementing object. The behaviour does not change based on the caller or the circumstances of the call. + + + The func value is saying that the operation is not intrinsic to the object but based on context or the caller to define. + ++ An interface is a language feature representing some contract: "I hereby guarantee I make the following methods available". + ++ The benefit of using interfaces is that you give the type a name (you would get that with records too) and you are more clearly expressing your intention (other components can implement the interface). + ++ Use interface values when: + + Well known interfaces already exist, e.g. io.Reader + + More than one behavior required + + Typically stateful + + Implementations non-trivial + + + Use function values when: + + Only one behavior + + Typically stateless + + In-line implementations typical + + + http://go-talks.appspot.com/github.com/ChrisHines/talks/non-orthogonal-choices-in-go/non-orthogonal-choices-in-go.slide#1 \ No newline at end of file diff --git a/interfaces/oop/final/Makefile b/interfaces/oop/final/Makefile new file mode 100644 index 0000000..a9f738d --- /dev/null +++ b/interfaces/oop/final/Makefile @@ -0,0 +1,5 @@ +r: + go run . < ../../logs/log.txt + +t: + time go run . < ../../logs/log.txt \ No newline at end of file diff --git a/interfaces/oop/final/analysis.go b/interfaces/oop/final/analysis.go new file mode 100644 index 0000000..45e5edd --- /dev/null +++ b/interfaces/oop/final/analysis.go @@ -0,0 +1,55 @@ +// For more tutorials: https://blog.learngoprogramming.com +// +// Copyright © 2018 Inanc Gumus +// Learn Go Programming Course +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ +// + +package main + +import "sort" + +type ( + groupFunc func(result) string + filterFunc func(result) bool +) + +type analysis struct { + sum map[string]result // metrics per domain + keys []string // unique keys + groupKey groupFunc + filter filterFunc +} + +func newAnalysis() *analysis { + return &analysis{ + sum: make(map[string]result), + groupKey: domainGrouper, + filter: noopFilter, + } +} + +func (a *analysis) groupBy(g groupFunc) { a.groupKey = g } +func (a *analysis) filterBy(f filterFunc) { a.filter = f } + +func (a *analysis) analyse(r result) { + if !a.filter(r) { + return + } + + key := a.groupKey(r) + if _, ok := a.sum[key]; !ok { + a.keys = append(a.keys, key) + } + + a.sum[key] = r.add(a.sum[key]) +} + +// each analysed result will be sent by the given func +func (a *analysis) each(f func(r result)) { + sort.Strings(a.keys) + + for _, domain := range a.keys { + f(a.sum[domain]) + } +} diff --git a/interfaces/oop/final/chartsummary.go b/interfaces/oop/final/chartsummary.go new file mode 100644 index 0000000..0064236 --- /dev/null +++ b/interfaces/oop/final/chartsummary.go @@ -0,0 +1,42 @@ +package main + +import ( + "os" + "strconv" + + c "github.com/wcharczuk/go-chart" +) + +type chartSummary struct { + title string + width, height int +} + +func (s *chartSummary) summarize(results iterator) error { + w := os.Stdout + + donut := c.DonutChart{ + Title: s.title, + TitleStyle: c.Style{ + FontSize: 35, + Show: true, + FontColor: c.ColorAlternateGreen, + }, + Width: s.width, + Height: s.height, + } + + results.each(func(r result) { + v := c.Value{ + Label: r.domain + r.page + ": " + strconv.Itoa(r.visits), + Value: float64(r.visits), + Style: c.Style{ + FontSize: 14, + }, + } + + donut.Values = append(donut.Values, v) + }) + + return donut.Render(c.SVG, w) +} diff --git a/interfaces/oop/final/filters.go b/interfaces/oop/final/filters.go new file mode 100644 index 0000000..c5032ef --- /dev/null +++ b/interfaces/oop/final/filters.go @@ -0,0 +1,34 @@ +package main + +import "strings" + +func noopFilter(r result) bool { + return true +} + +func notUsing(filter filterFunc) filterFunc { + return func(r result) bool { + return !filter(r) + } +} + +func domainExtFilter(domains ...string) filterFunc { + return func(r result) bool { + for _, domain := range domains { + if strings.HasSuffix(r.domain, "."+domain) { + return true + } + } + return false + } +} + +func domainFilter(domain string) filterFunc { + return func(r result) bool { + return strings.Contains(r.domain, domain) + } +} + +func orgDomainsFilter(r result) bool { + return strings.HasSuffix(r.domain, ".org") +} diff --git a/interfaces/oop/final/groupers.go b/interfaces/oop/final/groupers.go new file mode 100644 index 0000000..5bf2a17 --- /dev/null +++ b/interfaces/oop/final/groupers.go @@ -0,0 +1,13 @@ +package main + +// domainGrouper groups by domain. +// but it keeps the other fields. +// for example: it returns pages as well, but you shouldn't use them. +// exercise: write a function that erases the unnecessary data. +func domainGrouper(r result) string { + return r.domain +} + +func pageGrouper(r result) string { + return r.domain + r.page +} diff --git a/interfaces/oop/final/jsonparser.go b/interfaces/oop/final/jsonparser.go new file mode 100644 index 0000000..c08c393 --- /dev/null +++ b/interfaces/oop/final/jsonparser.go @@ -0,0 +1,69 @@ +// For more tutorials: https://bp.learngoprogramming.com +// +// Copyright © 2018 Inanc Gumus +// Learn Go Programming Course +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ +// + +package main + +import ( + "encoding/json" + "fmt" + "io" + "io/ioutil" +) + +type jsonParser struct { + r io.Reader + update func(r result) +} + +func newJSONParser(r io.Reader) *jsonParser { + return &jsonParser{ + r: r, + update: func(result) {}, + } +} + +func (p *jsonParser) parse() error { + defer readClose(p.r) + + bytes, err := ioutil.ReadAll(p.r) + if err != nil { + return err + } + + return p.parseJSON(bytes) +} + +func (p *jsonParser) parseJSON(bytes []byte) error { + var rs []struct { + Domain string + Page string + Visits int + Uniques int + } + + if err := json.Unmarshal(bytes, &rs); err != nil { + if serr, ok := err.(*json.SyntaxError); ok { + return fmt.Errorf("%v %q", serr, bytes[:serr.Offset]) + } + return err + } + + for _, r := range rs { + p.update(result{ + domain: r.Domain, + page: r.Page, + visits: r.Visits, + uniques: r.Uniques, + }) + } + + return nil +} + +func (p *jsonParser) notify(f func(r result)) { + p.update = f +} diff --git a/interfaces/oop/final/main.go b/interfaces/oop/final/main.go new file mode 100644 index 0000000..10f09d0 --- /dev/null +++ b/interfaces/oop/final/main.go @@ -0,0 +1,39 @@ +// For more tutorials: https://blog.learngoprogramming.com +// +// Copyright © 2018 Inanc Gumus +// Learn Go Programming Course +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ +// + +package main + +import ( + "log" + "os" +) + +func main() { + an := newAnalysis() + an.filterBy(notUsing(domainExtFilter("io", "com"))) + an.groupBy(domainGrouper) + + // pars, err := parseTextFile("log.txt") + // if err != nil { + // log.Fatalln(err) + // } + + err := report(an, newTextParser(os.Stdin), summarizeFunc(textSummary)) + // err := report(an, newJSONParser(os.Stdin), newTextSummary()) + + // chart := &chartSummary{ + // title: "visits per domain", + // width: 1920, + // height: 800, + // } + + // err := report(an, newTextParser(os.Stdin), chart) + + if err != nil { + log.Fatalln(err) + } +} diff --git a/interfaces/oop/final/readclose.go b/interfaces/oop/final/readclose.go new file mode 100644 index 0000000..1aa7a30 --- /dev/null +++ b/interfaces/oop/final/readclose.go @@ -0,0 +1,18 @@ +// For more tutorials: https://blog.learngoprogramming.com +// +// Copyright © 2018 Inanc Gumus +// Learn Go Programming Course +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ +// + +package main + +import ( + "io" +) + +func readClose(r io.Reader) { + if rc, ok := r.(io.Closer); ok { + rc.Close() + } +} diff --git a/interfaces/oop/final/report.go b/interfaces/oop/final/report.go new file mode 100644 index 0000000..46367ef --- /dev/null +++ b/interfaces/oop/final/report.go @@ -0,0 +1,40 @@ +// For more tutorials: https://blog.learngoprogramming.com +// +// Copyright © 2018 Inanc Gumus +// Learn Go Programming Course +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ +// + +package main + +type notifier interface { + notify(func(r result)) +} + +type parser interface { + parse() error + notifier +} + +type iterator interface { + each(func(result)) +} + +type analyser interface { + analyse(result) + iterator +} + +type summarizer interface { + summarize(iterator) error +} + +func report(a analyser, p parser, s summarizer) error { + p.notify(a.analyse) + + if err := p.parse(); err != nil { + return err + } + + return s.summarize(a) +} diff --git a/interfaces/oop/final/result.go b/interfaces/oop/final/result.go new file mode 100644 index 0000000..9906822 --- /dev/null +++ b/interfaces/oop/final/result.go @@ -0,0 +1,14 @@ +package main + +type result struct { + domain string + page string + visits int + uniques int +} + +func (r result) add(other result) result { + r.visits += other.visits + r.uniques += other.uniques + return r +} diff --git a/interfaces/oop/final/textparser.go b/interfaces/oop/final/textparser.go new file mode 100644 index 0000000..4f3c2a5 --- /dev/null +++ b/interfaces/oop/final/textparser.go @@ -0,0 +1,86 @@ +// For more tutorials: https://bp.learngoprogramming.com +// +// Copyright © 2018 Inanc Gumus +// Learn Go Programming Course +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ +// + +package main + +import ( + "bufio" + "fmt" + "io" + "os" + "strconv" + "strings" +) + +const fieldsLength = 4 + +type textParser struct { + r io.Reader + update func(r result) +} + +func newTextParser(r io.Reader) *textParser { + return &textParser{ + r: r, + update: func(result) {}, + } +} + +func parseTextFile(path string) (*textParser, error) { + f, err := os.Open(path) + if err != nil { + return nil, err + } + + return newTextParser(f), nil +} + +func (p *textParser) notify(f func(r result)) { + p.update = f +} + +func (p *textParser) parse() error { + defer readClose(p.r) + + var ( + l = 1 + in = bufio.NewScanner(p.r) + ) + + for in.Scan() { + r, err := p.parseFields(in.Text()) + if err != nil { + return fmt.Errorf("line %d: %v", l, err) + } + + p.update(r) + l++ + } + + return in.Err() +} + +func (p *textParser) parseFields(s string) (r result, err error) { + fields := strings.Fields(s) + if len(fields) != fieldsLength { + return r, fmt.Errorf("wrong number of fields %q", fields) + } + + r.domain, r.page = fields[0], fields[1] + + r.visits, err = strconv.Atoi(fields[2]) + if err != nil || r.visits < 0 { + return r, fmt.Errorf("wrong input %q", fields[2]) + } + + r.uniques, err = strconv.Atoi(fields[3]) + if err != nil || r.uniques < 0 { + return r, fmt.Errorf("wrong input %q", fields[3]) + } + + return r, nil +} diff --git a/interfaces/oop/final/textsummary.go b/interfaces/oop/final/textsummary.go new file mode 100644 index 0000000..fec1fcc --- /dev/null +++ b/interfaces/oop/final/textsummary.go @@ -0,0 +1,56 @@ +// For more tutorials: https://blog.learngoprogramming.com +// +// Copyright © 2018 Inanc Gumus +// Learn Go Programming Course +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ +// + +package main + +import ( + "fmt" + "os" + "text/tabwriter" +) + +// TODO: make this configurable? or exercise? +const ( + minWidth = 0 + tabWidth = 4 + padding = 4 + flags = 0 +) + +type summarizeFunc func(iterator) error + +func (f summarizeFunc) summarize(results iterator) error { + return f(results) +} + +// type textSummary struct{} + +// func newTextSummary() *textSummary { +// return new(textSummary) +// } + +// func (s *textSummary) summarize(results iterator) error { +func textSummary(results iterator) error { + w := tabwriter.NewWriter(os.Stdout, minWidth, tabWidth, padding, ' ', flags) + + write := fmt.Fprintf + + write(w, "DOMAINS\tPAGES\tVISITS\tUNIQUES\n") + write(w, "-------\t-----\t------\t-------\n") + + var total result + results.each(func(r result) { + total = total.add(r) + + write(w, "%s\t%s\t%d\t%d\n", r.domain, r.page, r.visits, r.uniques) + }) + + write(w, "\t\t\t\n") + write(w, "%s\t%s\t%d\t%d\n", "TOTAL", "", total.visits, total.uniques) + + return w.Flush() +} diff --git a/interfaces/refactor-notes/refactor-00/changes.md b/interfaces/refactor-notes/refactor-00/changes.md new file mode 100644 index 0000000..bf1a690 --- /dev/null +++ b/interfaces/refactor-notes/refactor-00/changes.md @@ -0,0 +1,34 @@ +## CHANGES + +### PROBLEM ++ adding new fields makes the code complex ++ needs to update: `result`, `parser`, `summarizer` ++ needs to add new fields to `parser`: `totalVisits` + `totalUniques` ++ in `parse()`: repeating line errors + + if we parsing out of it we'd need to have *parser — superfluous + +### SOLUTION ++ move all the result related logic to result.go + ++ move `parser.go/result` -> `result.go` + + move `parser.go/parsing` logic -> `result.go` + ++ add `addResult` -> `result.go` + + remove `parser struct`'s: `totalVisits`, `totalUniques` + + change `update()`'s last line: `p.sum[r.domain] = addResult` + ++ remove `(line #d)` errors from `result.go` + + add: `return r, err` — named params are error prone + + always check for the error first + + `if r.visits < 0 || err != nil` -> `if err != nil || r.visits < 0` + ++ `parser.go`: check the `parseFields()`: + ```golang + r, err := parseFields(line) + if err != nil { + p.lerr = fmt.Errorf("line %d: %v", p.lines, err) + }``` + ++ - `parser.go` and `summarize.go` + - remove `total int` + - let `summarize()` calculate the totals \ No newline at end of file diff --git a/interfaces/refactor-notes/refactor-00/main.go b/interfaces/refactor-notes/refactor-00/main.go new file mode 100644 index 0000000..43e763f --- /dev/null +++ b/interfaces/refactor-notes/refactor-00/main.go @@ -0,0 +1,36 @@ +// For more tutorials: https://blog.learngoprogramming.com +// +// Copyright © 2018 Inanc Gumus +// Learn Go Programming Course +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ +// + +package main + +import ( + "bufio" + "fmt" + "os" +) + +func main() { + p := newParser() + + in := bufio.NewScanner(os.Stdin) + for in.Scan() { + parsed := parse(p, in.Text()) + update(p, parsed) + } + + summarize(p) + dumpErrs([]error{in.Err(), err(p)}) +} + +// dumpErrs simplifies handling multiple errors +func dumpErrs(errs []error) { + for _, err := range errs { + if err != nil { + fmt.Println("> Err:", err) + } + } +} diff --git a/interfaces/refactor-notes/refactor-00/parser.go b/interfaces/refactor-notes/refactor-00/parser.go new file mode 100644 index 0000000..d347e35 --- /dev/null +++ b/interfaces/refactor-notes/refactor-00/parser.go @@ -0,0 +1,74 @@ +// For more tutorials: https://blog.learngoprogramming.com +// +// Copyright © 2018 Inanc Gumus +// Learn Go Programming Course +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ +// + +package main + +import ( + "fmt" +) + +// parser keeps track of the parsing +type parser struct { + sum map[string]result // metrics per domain + domains []string // unique domain names + lines int // number of parsed lines (for the error messages) + lerr error // the last error occurred + + // totalVisits int // total visits for all domains + // totalUniques int // total uniques for all domains +} + +// newParser constructs, initializes and returns a new parser +func newParser() *parser { + return &parser{sum: make(map[string]result)} +} + +// parse a log line and return the result +func parse(p *parser, line string) (r result) { + if p.lerr != nil { + return + } + + p.lines++ + + r, err := parseResult(line) + if err != nil { + p.lerr = fmt.Errorf("line %d: %v", p.lines, err) + } + + return r +} + +// update the parsing results +func update(p *parser, r result) { + if p.lerr != nil { + return + } + + // Collect the unique domains + cur, ok := p.sum[r.domain] + if !ok { + p.domains = append(p.domains, r.domain) + } + + // Keep track of total and per domain visits + // p.totalVisits += r.visits + // p.totalUniques += r.uniques + + // create and assign a new copy of `visit` + // p.sum[r.domain] = result{ + // domain: r.domain, + // visits: r.visits + cur.visits, + // uniques: r.uniques + cur.uniques, + // } + p.sum[r.domain] = addResult(r, cur) +} + +// err returns the last error encountered +func err(p *parser) error { + return p.lerr +} diff --git a/interfaces/refactor-notes/refactor-00/result.go b/interfaces/refactor-notes/refactor-00/result.go new file mode 100644 index 0000000..83c577f --- /dev/null +++ b/interfaces/refactor-notes/refactor-00/result.go @@ -0,0 +1,53 @@ +// For more tutorials: https://blog.learngoprogramming.com +// +// Copyright © 2018 Inanc Gumus +// Learn Go Programming Course +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ +// + +package main + +import ( + "fmt" + "strconv" + "strings" +) + +const fieldsLength = 4 + +// result stores the parsed result for a domain +type result struct { + domain, page string + visits, uniques int + // add more metrics if needed +} + +// parseResult from a log line +func parseResult(line string) (r result, err error) { + fields := strings.Fields(line) + if len(fields) != fieldsLength { + return r, fmt.Errorf("wrong input: %v", fields) + } + + r.domain = fields[0] + r.page = fields[1] + + r.visits, err = strconv.Atoi(fields[2]) + if err != nil || r.visits < 0 { + return r, fmt.Errorf("wrong input: %q", fields[2]) + } + + r.uniques, err = strconv.Atoi(fields[3]) + if err != nil || r.uniques < 0 { + return r, fmt.Errorf("wrong input: %q", fields[3]) + } + + return r, nil +} + +// addResult to another one +func addResult(r, other result) result { + r.visits += other.visits + r.uniques += other.uniques + return r +} diff --git a/interfaces/refactor-notes/refactor-00/summarize.go b/interfaces/refactor-notes/refactor-00/summarize.go new file mode 100644 index 0000000..5d934ab --- /dev/null +++ b/interfaces/refactor-notes/refactor-00/summarize.go @@ -0,0 +1,43 @@ +// For more tutorials: https://blog.learngoprogramming.com +// +// Copyright © 2018 Inanc Gumus +// Learn Go Programming Course +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ +// + +package main + +import ( + "fmt" + "sort" + "strings" +) + +const ( + // DOMAINS PAGES VISITS UNIQUES + // ^ ^ ^ ^ + // | | | | + header = "%-25s %-10s %10s %10s\n" + line = "%-25s %-10s %10d %10d\n" + footer = "\n%-36s %10d %10d\n" // -> "" VISITS UNIQUES + dash = "-" + dashLength = 58 +) + +// summarize summarizes and prints the parsing result +func summarize(p *parser) { + sort.Strings(p.domains) + + fmt.Printf(header, "DOMAIN", "PAGES", "VISITS", "UNIQUES") + fmt.Println(strings.Repeat("-", dashLength)) + + var total result + + for _, domain := range p.domains { + r := p.sum[domain] + total = addResult(total, r) + + fmt.Printf(line, r.domain, r.page, r.visits, r.uniques) + } + fmt.Printf(footer, "TOTAL", total.visits, total.uniques) +} diff --git a/interfaces/refactor-notes/refactor-01/changes.md b/interfaces/refactor-notes/refactor-01/changes.md new file mode 100644 index 0000000..703434f --- /dev/null +++ b/interfaces/refactor-notes/refactor-01/changes.md @@ -0,0 +1,61 @@ +### PROBLEM ++ `main.go` (api client) does a lot of things: + + read the log input + + parse line by line + + updates the results + + display the results + ++ inflexible: + + filter by extension (can change) + + group by domain (can change) — group by page? + +## SOLUTION ++ hide the parsing api from the client + ++ move `main.go/scanner` -> `parser.go/parse()` + + add `main.go`: err handling from `parse()` + ++ `parser.go/parse()` -> return err directly + + remove: `if p.lerr != nil { return }` from parse() and update() + + remove: `dumpErrs` + + remove: `parser.go/err()` + + remove `parser.go/lerr` + + return `in.Err()` from `parse()` + + + remove: `p.lines++` + + `return r, fmt.Errorf("line %d: %v", p.lines, err)` + + remove: `lines int` + + `parse()` and `parse()` becomes: + ```golang + func parse(p *parser, line string) (result, error) { + return parseFields(line) + } + + func parse(p *parser) { + // ... + r, err := parse(p, in.Text()) + if err != nil { + return fmt.Errorf("line %d: %v", p.lines, err) + } + // ... + } + ``` + + + remove `parse()` + + call `parseFields` directly in `parse()`: + ```go + var ( + l = 1 + in = bufio.NewScanner(os.Stdin) + ) + + for in.Scan() { + r, err := parseFields(in.Text()) + if err != nil { + return fmt.Errorf("line %d: %v", l, err) + } + + update(p, r) + l++ + } + ``` \ No newline at end of file diff --git a/interfaces/refactor-notes/refactor-01/main.go b/interfaces/refactor-notes/refactor-01/main.go new file mode 100644 index 0000000..c11aa06 --- /dev/null +++ b/interfaces/refactor-notes/refactor-01/main.go @@ -0,0 +1,23 @@ +// For more tutorials: https://blog.learngoprogramming.com +// +// Copyright © 2018 Inanc Gumus +// Learn Go Programming Course +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ +// + +package main + +import ( + "fmt" +) + +func main() { + p := newParser() + + if err := parse(p); err != nil { + fmt.Println("> Err:", err) + return + } + + summarize(p) +} diff --git a/interfaces/refactor-notes/refactor-01/parser.go b/interfaces/refactor-notes/refactor-01/parser.go new file mode 100644 index 0000000..a0aee50 --- /dev/null +++ b/interfaces/refactor-notes/refactor-01/parser.go @@ -0,0 +1,57 @@ +// For more tutorials: https://blog.learngoprogramming.com +// +// Copyright © 2018 Inanc Gumus +// Learn Go Programming Course +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ +// + +package main + +import ( + "bufio" + "fmt" + "os" +) + +// parser keeps track of the parsing +type parser struct { + sum map[string]result // metrics per domain + domains []string // unique domain names +} + +// newParser constructs, initializes and returns a new parser +func newParser() *parser { + return &parser{sum: make(map[string]result)} +} + +// parse the log lines and return results +func parse(p *parser) error { + var ( + l = 1 + in = bufio.NewScanner(os.Stdin) + ) + + for in.Scan() { + r, err := parseResult(in.Text()) + if err != nil { + return fmt.Errorf("line %d: %v", l, err) + } + + l++ + + update(p, r) + } + + return in.Err() +} + +// update the parsing results +func update(p *parser, r result) { + // Collect the unique domains + if _, ok := p.sum[r.domain]; !ok { + p.domains = append(p.domains, r.domain) + } + + // create and assign a new copy of `visit` + p.sum[r.domain] = addResult(r, p.sum[r.domain]) +} diff --git a/interfaces/refactor-notes/refactor-01/result.go b/interfaces/refactor-notes/refactor-01/result.go new file mode 100644 index 0000000..83c577f --- /dev/null +++ b/interfaces/refactor-notes/refactor-01/result.go @@ -0,0 +1,53 @@ +// For more tutorials: https://blog.learngoprogramming.com +// +// Copyright © 2018 Inanc Gumus +// Learn Go Programming Course +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ +// + +package main + +import ( + "fmt" + "strconv" + "strings" +) + +const fieldsLength = 4 + +// result stores the parsed result for a domain +type result struct { + domain, page string + visits, uniques int + // add more metrics if needed +} + +// parseResult from a log line +func parseResult(line string) (r result, err error) { + fields := strings.Fields(line) + if len(fields) != fieldsLength { + return r, fmt.Errorf("wrong input: %v", fields) + } + + r.domain = fields[0] + r.page = fields[1] + + r.visits, err = strconv.Atoi(fields[2]) + if err != nil || r.visits < 0 { + return r, fmt.Errorf("wrong input: %q", fields[2]) + } + + r.uniques, err = strconv.Atoi(fields[3]) + if err != nil || r.uniques < 0 { + return r, fmt.Errorf("wrong input: %q", fields[3]) + } + + return r, nil +} + +// addResult to another one +func addResult(r, other result) result { + r.visits += other.visits + r.uniques += other.uniques + return r +} diff --git a/interfaces/refactor-notes/refactor-01/summarize.go b/interfaces/refactor-notes/refactor-01/summarize.go new file mode 100644 index 0000000..1a13b48 --- /dev/null +++ b/interfaces/refactor-notes/refactor-01/summarize.go @@ -0,0 +1,48 @@ +// For more tutorials: https://blog.learngoprogramming.com +// +// Copyright © 2018 Inanc Gumus +// Learn Go Programming Course +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ +// + +package main + +import ( + "fmt" + "sort" + "strings" +) + +// summarize summarizes and prints the parsing result +// + violation: accesses the parsing internals: p.domains + p.sum + p.total +// + give it the []result only. +// + let it calculate the total. +const ( + + // DOMAINS PAGES VISITS UNIQUES + // ^ ^ ^ ^ + // | | | | + header = "%-25s %-10s %10s %10s\n" + line = "%-25s %-10s %10d %10d\n" + footer = "\n%-36s %10d %10d\n" // -> "" VISITS UNIQUES + dash = "-" + dashLength = 58 +) + +// summarize summarizes and prints the parsing result +func summarize(p *parser) { + sort.Strings(p.domains) + + fmt.Printf(header, "DOMAIN", "PAGES", "VISITS", "UNIQUES") + fmt.Println(strings.Repeat("-", dashLength)) + + var total result + + for _, domain := range p.domains { + r := p.sum[domain] + total = addResult(total, r) + + fmt.Printf(line, r.domain, r.page, r.visits, r.uniques) + } + fmt.Printf(footer, "TOTAL", total.visits, total.uniques) +} diff --git a/interfaces/refactor-notes/refactor-02/changes.md b/interfaces/refactor-notes/refactor-02/changes.md new file mode 100644 index 0000000..ae7d286 --- /dev/null +++ b/interfaces/refactor-notes/refactor-02/changes.md @@ -0,0 +1,20 @@ +### PROBLEM ++ `summarize()` knows a lot about the internals of the `parser`. + + coupled to the `parser`. + +## SOLUTION ++ remove: `parser.go` `sum` and `domains` fields + + remove: `parser.go/newParser()` + + change: `parser.go/parse(p *parser) error` -> `parse() ([]result, error)` + + initialize: `sum` inside `parse()` + + remove: `update()` + + call: `sum` update in the `parse()` + + collect the grouped results and return them from `parser()` + ++ `summarize(p *parser)` -> `summarize([]result)` ++ in `summarize()` + + `sort.Slice` + + range over `[]result` + ++ `main.go` + + just: `res, err := parse()` \ No newline at end of file diff --git a/interfaces/refactor-notes/refactor-02/main.go b/interfaces/refactor-notes/refactor-02/main.go new file mode 100644 index 0000000..636834f --- /dev/null +++ b/interfaces/refactor-notes/refactor-02/main.go @@ -0,0 +1,22 @@ +// For more tutorials: https://blog.learngoprogramming.com +// +// Copyright © 2018 Inanc Gumus +// Learn Go Programming Course +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ +// + +package main + +import ( + "fmt" +) + +func main() { + res, err := parse() + if err != nil { + fmt.Println("> Err:", err) + return + } + + summarize(res) +} diff --git a/interfaces/refactor-notes/refactor-02/parser.go b/interfaces/refactor-notes/refactor-02/parser.go new file mode 100644 index 0000000..16ab798 --- /dev/null +++ b/interfaces/refactor-notes/refactor-02/parser.go @@ -0,0 +1,48 @@ +// For more tutorials: https://blog.learngoprogramming.com +// +// Copyright © 2018 Inanc Gumus +// Learn Go Programming Course +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ +// + +package main + +import ( + "bufio" + "fmt" + "os" +) + +// parser keeps track of the parsing +type parser struct { +} + +// parse the log lines and return results +func parse() ([]result, error) { + var ( + l = 1 + in = bufio.NewScanner(os.Stdin) + sum = make(map[string]result) + ) + + // parse the log lines + for in.Scan() { + r, err := parseResult(in.Text()) + if err != nil { + return nil, fmt.Errorf("line %d: %v", l, err) + } + + l++ + + // group the log lines by domain + sum[r.domain] = addResult(r, sum[r.domain]) + } + + // collect the grouped results + res := make([]result, 0, len(sum)) + for _, r := range sum { + res = append(res, r) + } + + return res, in.Err() +} diff --git a/interfaces/refactor-notes/refactor-02/result.go b/interfaces/refactor-notes/refactor-02/result.go new file mode 100644 index 0000000..83c577f --- /dev/null +++ b/interfaces/refactor-notes/refactor-02/result.go @@ -0,0 +1,53 @@ +// For more tutorials: https://blog.learngoprogramming.com +// +// Copyright © 2018 Inanc Gumus +// Learn Go Programming Course +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ +// + +package main + +import ( + "fmt" + "strconv" + "strings" +) + +const fieldsLength = 4 + +// result stores the parsed result for a domain +type result struct { + domain, page string + visits, uniques int + // add more metrics if needed +} + +// parseResult from a log line +func parseResult(line string) (r result, err error) { + fields := strings.Fields(line) + if len(fields) != fieldsLength { + return r, fmt.Errorf("wrong input: %v", fields) + } + + r.domain = fields[0] + r.page = fields[1] + + r.visits, err = strconv.Atoi(fields[2]) + if err != nil || r.visits < 0 { + return r, fmt.Errorf("wrong input: %q", fields[2]) + } + + r.uniques, err = strconv.Atoi(fields[3]) + if err != nil || r.uniques < 0 { + return r, fmt.Errorf("wrong input: %q", fields[3]) + } + + return r, nil +} + +// addResult to another one +func addResult(r, other result) result { + r.visits += other.visits + r.uniques += other.uniques + return r +} diff --git a/interfaces/refactor-notes/refactor-02/summarize.go b/interfaces/refactor-notes/refactor-02/summarize.go new file mode 100644 index 0000000..242c760 --- /dev/null +++ b/interfaces/refactor-notes/refactor-02/summarize.go @@ -0,0 +1,49 @@ +// For more tutorials: https://blog.learngoprogramming.com +// +// Copyright © 2018 Inanc Gumus +// Learn Go Programming Course +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ +// + +package main + +import ( + "fmt" + "sort" + "strings" +) + +// summarize summarizes and prints the parsing result +// + violation: accesses the parsing internals: p.domains + p.sum + p.total +// + give it the []result only. +// + let it calculate the total. +const ( + + // DOMAINS PAGES VISITS UNIQUES + // ^ ^ ^ ^ + // | | | | + header = "%-25s %-10s %10s %10s\n" + line = "%-25s %-10s %10d %10d\n" + footer = "\n%-36s %10d %10d\n" // -> "" VISITS UNIQUES + dash = "-" + dashLength = 58 +) + +// summarize summarizes and prints the parsing result +func summarize(res []result) { + // sort.Strings(p.domains) + sort.Slice(res, func(i, j int) bool { + return res[i].domain <= res[j].domain + }) + + fmt.Printf(header, "DOMAIN", "PAGES", "VISITS", "UNIQUES") + fmt.Println(strings.Repeat("-", dashLength)) + + var total result + + for _, r := range res { + total = addResult(total, r) + fmt.Printf(line, r.domain, r.page, r.visits, r.uniques) + } + fmt.Printf(footer, "TOTAL", total.visits, total.uniques) +} diff --git a/interfaces/refactor-notes/refactor-03-deprecated/changes.md b/interfaces/refactor-notes/refactor-03-deprecated/changes.md new file mode 100644 index 0000000..7665010 --- /dev/null +++ b/interfaces/refactor-notes/refactor-03-deprecated/changes.md @@ -0,0 +1,14 @@ +### PROBLEM ++ `parser.go/parse()` also does updating. back to square one. + + we need to extract the reusable behavior: scanning. + ++ inflexible: + + adding a filter is hard. needs to change the `scan()` code. + + adding a grouper is also hard. domain grouping is hardcoded. + +## SOLUTION ++ + +## IDEAS: + ++ make domain filter accept variadic args \ No newline at end of file diff --git a/interfaces/refactor-notes/refactor-03-deprecated/main.go b/interfaces/refactor-notes/refactor-03-deprecated/main.go new file mode 100644 index 0000000..ec21e96 --- /dev/null +++ b/interfaces/refactor-notes/refactor-03-deprecated/main.go @@ -0,0 +1,36 @@ +// For more tutorials: https://blog.learngoprogramming.com +// +// Copyright © 2018 Inanc Gumus +// Learn Go Programming Course +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ +// + +package main + +import ( + "fmt" +) + +/* +p := pipeline{ + read: textReader(os.Stdin), + write: textWriter(os.Stdout), + filterBy: notUsing(domainExtFilter("io")), + groupBy: domainGrouper, +} + +if err := start(p); err != nil { + fmt.Println("> Err:", err) +} +*/ + +func main() { + p := newParser() + + if err := parse(p); err != nil { + fmt.Println("> Err:", err) + return + } + + summarize(p) +} diff --git a/interfaces/refactor-notes/refactor-03-deprecated/parser.go b/interfaces/refactor-notes/refactor-03-deprecated/parser.go new file mode 100644 index 0000000..4fb06d4 --- /dev/null +++ b/interfaces/refactor-notes/refactor-03-deprecated/parser.go @@ -0,0 +1,40 @@ +// For more tutorials: https://blog.learngoprogramming.com +// +// Copyright © 2018 Inanc Gumus +// Learn Go Programming Course +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ +// + +package main + +// parser keeps track of the parsing +type parser struct { + sum map[string]result // metrics per domain + domains []string // unique domain names +} + +// newParser constructs, initializes and returns a new parser +func newParser() *parser { + return &parser{sum: make(map[string]result)} +} + +// parse all the log lines and update the results +func parse(p *parser) error { + process := func(r result) { + update(p, r) + } + + err := scan(process) + + return err +} + +func update(p *parser, r result) { + // Collect the unique domains + if _, ok := p.sum[r.domain]; !ok { + p.domains = append(p.domains, r.domain) + } + + // create and assign a new copy of `visit` + p.sum[r.domain] = addResult(r, p.sum[r.domain]) +} diff --git a/interfaces/refactor-notes/refactor-03-deprecated/result.go b/interfaces/refactor-notes/refactor-03-deprecated/result.go new file mode 100644 index 0000000..83c577f --- /dev/null +++ b/interfaces/refactor-notes/refactor-03-deprecated/result.go @@ -0,0 +1,53 @@ +// For more tutorials: https://blog.learngoprogramming.com +// +// Copyright © 2018 Inanc Gumus +// Learn Go Programming Course +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ +// + +package main + +import ( + "fmt" + "strconv" + "strings" +) + +const fieldsLength = 4 + +// result stores the parsed result for a domain +type result struct { + domain, page string + visits, uniques int + // add more metrics if needed +} + +// parseResult from a log line +func parseResult(line string) (r result, err error) { + fields := strings.Fields(line) + if len(fields) != fieldsLength { + return r, fmt.Errorf("wrong input: %v", fields) + } + + r.domain = fields[0] + r.page = fields[1] + + r.visits, err = strconv.Atoi(fields[2]) + if err != nil || r.visits < 0 { + return r, fmt.Errorf("wrong input: %q", fields[2]) + } + + r.uniques, err = strconv.Atoi(fields[3]) + if err != nil || r.uniques < 0 { + return r, fmt.Errorf("wrong input: %q", fields[3]) + } + + return r, nil +} + +// addResult to another one +func addResult(r, other result) result { + r.visits += other.visits + r.uniques += other.uniques + return r +} diff --git a/interfaces/refactor-notes/refactor-03-deprecated/scanner.go b/interfaces/refactor-notes/refactor-03-deprecated/scanner.go new file mode 100644 index 0000000..fc37b36 --- /dev/null +++ b/interfaces/refactor-notes/refactor-03-deprecated/scanner.go @@ -0,0 +1,35 @@ +// For more tutorials: https://blog.learngoprogramming.com +// +// Copyright © 2018 Inanc Gumus +// Learn Go Programming Course +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ +// + +package main + +import ( + "bufio" + "fmt" + "os" +) + +type processFn func(r result) + +func scan(process processFn) error { + var ( + l = 1 + in = bufio.NewScanner(os.Stdin) + ) + + for in.Scan() { + r, err := parseResult(in.Text()) + if err != nil { + return fmt.Errorf("line %d: %v", l, err) + } + + l++ + + process(r) + } + return in.Err() +} diff --git a/interfaces/refactor-notes/refactor-03-deprecated/summarize.go b/interfaces/refactor-notes/refactor-03-deprecated/summarize.go new file mode 100644 index 0000000..1a13b48 --- /dev/null +++ b/interfaces/refactor-notes/refactor-03-deprecated/summarize.go @@ -0,0 +1,48 @@ +// For more tutorials: https://blog.learngoprogramming.com +// +// Copyright © 2018 Inanc Gumus +// Learn Go Programming Course +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ +// + +package main + +import ( + "fmt" + "sort" + "strings" +) + +// summarize summarizes and prints the parsing result +// + violation: accesses the parsing internals: p.domains + p.sum + p.total +// + give it the []result only. +// + let it calculate the total. +const ( + + // DOMAINS PAGES VISITS UNIQUES + // ^ ^ ^ ^ + // | | | | + header = "%-25s %-10s %10s %10s\n" + line = "%-25s %-10s %10d %10d\n" + footer = "\n%-36s %10d %10d\n" // -> "" VISITS UNIQUES + dash = "-" + dashLength = 58 +) + +// summarize summarizes and prints the parsing result +func summarize(p *parser) { + sort.Strings(p.domains) + + fmt.Printf(header, "DOMAIN", "PAGES", "VISITS", "UNIQUES") + fmt.Println(strings.Repeat("-", dashLength)) + + var total result + + for _, domain := range p.domains { + r := p.sum[domain] + total = addResult(total, r) + + fmt.Printf(line, r.domain, r.page, r.visits, r.uniques) + } + fmt.Printf(footer, "TOTAL", total.visits, total.uniques) +} diff --git a/interfaces/refactor-notes/refactor-03/changes.md b/interfaces/refactor-notes/refactor-03/changes.md new file mode 100644 index 0000000..d3ff67e --- /dev/null +++ b/interfaces/refactor-notes/refactor-03/changes.md @@ -0,0 +1,7 @@ +### PROBLEM ++ ... + +## SOLUTION ++ `parser struct` -> `pipeline struct` ++ `parse()` -> `pipe(pipeline)` + diff --git a/interfaces/refactor-notes/refactor-03/main.go b/interfaces/refactor-notes/refactor-03/main.go new file mode 100644 index 0000000..b218604 --- /dev/null +++ b/interfaces/refactor-notes/refactor-03/main.go @@ -0,0 +1,29 @@ +// For more tutorials: https://blog.learngoprogramming.com +// +// Copyright © 2018 Inanc Gumus +// Learn Go Programming Course +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ +// + +package main + +import ( + "fmt" +) + +func main() { + pipes := pipeline{ + // read: textParser(), + // write: textSummary(), + // filterBy: notUsing(domainExtFilter("io", "com")), + // groupBy: domainGrouper, + } + + res, err := pipe(pipes) + if err != nil { + fmt.Println("> Err:", err) + return + } + + summarize(res) +} diff --git a/interfaces/refactor-notes/refactor-03/parser.go b/interfaces/refactor-notes/refactor-03/parser.go new file mode 100644 index 0000000..0e04e4b --- /dev/null +++ b/interfaces/refactor-notes/refactor-03/parser.go @@ -0,0 +1,48 @@ +// For more tutorials: https://blog.learngoprogramming.com +// +// Copyright © 2018 Inanc Gumus +// Learn Go Programming Course +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ +// + +package main + +import ( + "bufio" + "fmt" + "os" +) + +// pipeline determines the behavior of log processing +type pipeline struct { +} + +// pipe the log lines through funcs and produce a result +func pipe(opts pipeline) ([]result, error) { + var ( + l = 1 + in = bufio.NewScanner(os.Stdin) + sum = make(map[string]result) + ) + + // parse the log lines + for in.Scan() { + r, err := parseResult(in.Text()) + if err != nil { + return nil, fmt.Errorf("line %d: %v", l, err) + } + + l++ + + // group the log lines by domain + sum[r.domain] = addResult(r, sum[r.domain]) + } + + // collect the grouped results + res := make([]result, 0, len(sum)) + for _, r := range sum { + res = append(res, r) + } + + return res, in.Err() +} diff --git a/interfaces/refactor-notes/refactor-03/result.go b/interfaces/refactor-notes/refactor-03/result.go new file mode 100644 index 0000000..83c577f --- /dev/null +++ b/interfaces/refactor-notes/refactor-03/result.go @@ -0,0 +1,53 @@ +// For more tutorials: https://blog.learngoprogramming.com +// +// Copyright © 2018 Inanc Gumus +// Learn Go Programming Course +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ +// + +package main + +import ( + "fmt" + "strconv" + "strings" +) + +const fieldsLength = 4 + +// result stores the parsed result for a domain +type result struct { + domain, page string + visits, uniques int + // add more metrics if needed +} + +// parseResult from a log line +func parseResult(line string) (r result, err error) { + fields := strings.Fields(line) + if len(fields) != fieldsLength { + return r, fmt.Errorf("wrong input: %v", fields) + } + + r.domain = fields[0] + r.page = fields[1] + + r.visits, err = strconv.Atoi(fields[2]) + if err != nil || r.visits < 0 { + return r, fmt.Errorf("wrong input: %q", fields[2]) + } + + r.uniques, err = strconv.Atoi(fields[3]) + if err != nil || r.uniques < 0 { + return r, fmt.Errorf("wrong input: %q", fields[3]) + } + + return r, nil +} + +// addResult to another one +func addResult(r, other result) result { + r.visits += other.visits + r.uniques += other.uniques + return r +} diff --git a/interfaces/refactor-notes/refactor-03/summarize.go b/interfaces/refactor-notes/refactor-03/summarize.go new file mode 100644 index 0000000..6a0ec8b --- /dev/null +++ b/interfaces/refactor-notes/refactor-03/summarize.go @@ -0,0 +1,48 @@ +// For more tutorials: https://blog.learngoprogramming.com +// +// Copyright © 2018 Inanc Gumus +// Learn Go Programming Course +// License: https://creativecommons.org/licenses/by-nc-sa/4.0/ +// + +package main + +import ( + "fmt" + "sort" + "strings" +) + +// summarize summarizes and prints the parsing result +// + violation: accesses the parsing internals: p.domains + p.sum + p.total +// + give it the []result only. +// + let it calculate the total. +const ( + + // DOMAINS PAGES VISITS UNIQUES + // ^ ^ ^ ^ + // | | | | + header = "%-25s %-10s %10s %10s\n" + line = "%-25s %-10s %10d %10d\n" + footer = "\n%-36s %10d %10d\n" // -> "" VISITS UNIQUES + dash = "-" + dashLength = 58 +) + +// summarize summarizes and prints the parsing result +func summarize(res []result) { + sort.Slice(res, func(i, j int) bool { + return res[i].domain <= res[j].domain + }) + + fmt.Printf(header, "DOMAIN", "PAGES", "VISITS", "UNIQUES") + fmt.Println(strings.Repeat("-", dashLength)) + + var total result + + for _, r := range res { + total = addResult(total, r) + fmt.Printf(line, r.domain, r.page, r.visits, r.uniques) + } + fmt.Printf(footer, "TOTAL", total.visits, total.uniques) +}