interfaces: refactor

This commit is contained in:
Inanc Gumus
2019-08-19 10:21:11 +03:00
parent b95be49711
commit 158f475a2d
89 changed files with 784 additions and 2 deletions

View File

@ -0,0 +1,36 @@
# Problem
## All these functions operate on a *parser value:
+ `main.go`:
+ `summarize(*p parser)`
+ `parser.go`:
+ `func parse(p *parser, line string) (r result)`
+ `func update(p *parser, r result)`
+ `func err(p *parser) error`
## What is an object in Go?
+ Object = A value of a type : `&parser{}` is an object.
+ `type parser struct` is the definition (class/blueprint) of the object.
### Methods are behaviors of objects:
+ Parser needs to parse, update (analyse) the parsings, and report an error if any.
+ Parser also needs to summarize the results.
+ So, all these functions belong to a parser object.
# Solution
+ Attach the functions to the parser using methods.
+ Methods are the behavior of the parser.
+ Keep the methods that belong to a single type in the same file.
1. Move the `summarize` into `parser.go`.
2. `parser.go`: Use methods.
3. `main()` : Use methods of the `parser`.
# Convince
+ `p.` shows the fields and methods belong to the `parser`.
+ `p.` selects a method from the method set of the `parser`.
+ `p.method` sends the `p` as the first parameter to the method.
+ `p` is the receiver: `*pointer`. It's a pointer receiver.
+ Receiver is copied to the method.

View File

@ -0,0 +1,16 @@
learngoprogramming.com 10
learngoprogramming.com 15
learngoprogramming.com 10
learngoprogramming.com 20
learngoprogramming.com 5
golang.org 40
golang.org 20
golang.org 45
golang.org 15
blog.golang.org 60
blog.golang.org 30
blog.golang.org 20
blog.golang.org 65
blog.golang.org 15
inanc.io 30
inanc.io 70

View File

@ -0,0 +1,16 @@
learngoprogramming.com 10
learngoprogramming.com 15
learngoprogramming.com 10
learngoprogramming.com 20
learngoprogramming.com
golang.org 40
golang.org 20
golang.org 45
golang.org 15
blog.golang.org 60
blog.golang.org 30
blog.golang.org 20
blog.golang.org 65
blog.golang.org 15
inanc.io 30
inanc.io 70

View File

@ -0,0 +1,16 @@
learngoprogramming.com 10
learngoprogramming.com 15
learngoprogramming.com 10
learngoprogramming.com 20
learngoprogramming.com 5
golang.org 40
golang.org 20
golang.org -50
golang.org 15
blog.golang.org 60
blog.golang.org 30
blog.golang.org 20
blog.golang.org 65
blog.golang.org 15
inanc.io 30
inanc.io 70

View File

@ -0,0 +1,16 @@
learngoprogramming.com 10
learngoprogramming.com 15
learngoprogramming.com 10
learngoprogramming.com 20
learngoprogramming.com 5
golang.org FORTY
golang.org 20
golang.org 45
golang.org 15
blog.golang.org 60
blog.golang.org 30
blog.golang.org 20
blog.golang.org 65
blog.golang.org 15
inanc.io 30
inanc.io 70

View File

@ -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"
"os"
)
func main() {
p := newParser()
fmt.Printf("%T\n", (*parser).parse)
return
in := bufio.NewScanner(os.Stdin)
for in.Scan() {
parsed := p.parse(in.Text())
p.update(parsed)
}
p.summarize()
dumpErrs([]error{in.Err(), p.err()})
}
// dumpErrs together to simplify multiple error handling
func dumpErrs(errs []error) {
for _, err := range errs {
if err != nil {
fmt.Println("> Err:", err)
}
}
}

View File

@ -0,0 +1,100 @@
// 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"
"strconv"
"strings"
)
// result stores details about a log line
type result struct {
domain string
visits int
// add more metrics when needed
}
// parser keep tracks of the parsing
type parser struct {
sum map[string]result // metrics per domain
domains []string // unique domain names
total int // total visits for all domains
lines int // number of parsed lines (for the error messages)
lerr error // the last error occurred
}
// newParser creates and returns a new parser
func newParser() *parser {
return &parser{sum: make(map[string]result)}
}
// parse result from a log line
func (p *parser) parse(line string) (r result) {
if p.lerr != nil {
return
}
p.lines++
fields := strings.Fields(line)
if len(fields) != 2 {
p.lerr = fmt.Errorf("wrong input: %v (line #%d)", fields, p.lines)
return
}
var err error
r.domain = fields[0]
r.visits, err = strconv.Atoi(fields[1])
if r.visits < 0 || err != nil {
p.lerr = fmt.Errorf("wrong input: %q (line #%d)", fields[1], p.lines)
}
return
}
// update the parsing results
func (p *parser) update(r result) {
if p.lerr != nil {
return
}
// collect unique domains for easy access to sum map
if _, ok := p.sum[r.domain]; !ok {
p.domains = append(p.domains, r.domain)
}
// keep track of total visits
p.total += r.visits
// group the log lines by domain
p.sum[r.domain] = result{
domain: r.domain,
visits: r.visits + p.sum[r.domain].visits,
}
}
// summarize the parsing results
func (p *parser) summarize() {
sort.Strings(p.domains)
fmt.Printf("%-30s %10s\n", "DOMAIN", "VISITS")
fmt.Println(strings.Repeat("-", 45))
for _, domain := range p.domains {
fmt.Printf("%-30s %10d\n", domain, p.sum[domain].visits)
}
fmt.Printf("\n%-30s %10d\n", "TOTAL", p.total)
}
// err returns the last error encountered
func (p *parser) err() error {
return p.lerr
}

View File

@ -0,0 +1,7 @@
SHELL := /bin/bash
r:
go run . < ../logs/log.txt
t:
time go run . < ../logs/log.txt

View File

@ -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)
}

View File

@ -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
}

View File

@ -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")
}

View File

@ -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 ""
}

View File

@ -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
// }
// }

View File

@ -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)
}

View File

@ -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
}

View File

@ -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()
}
}

View File

@ -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
}
}

View File

@ -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)

View File

@ -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}
]

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,5 @@
r:
go run . < ../../logs/log.txt
t:
time go run . < ../../logs/log.txt

View File

@ -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])
}
}

View File

@ -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)
}

View File

@ -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")
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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()
}
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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()
}

View File

@ -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

View File

@ -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)
}
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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++
}
```

View File

@ -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)
}

View File

@ -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])
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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()`

View File

@ -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)
}

View File

@ -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()
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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

View File

@ -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)
}

View File

@ -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])
}

View File

@ -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
}

View File

@ -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()
}

View File

@ -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)
}

View File

@ -0,0 +1,7 @@
### PROBLEM
+ ...
## SOLUTION
+ `parser struct` -> `pipeline struct`
+ `parse()` -> `pipe(pipeline)`

View File

@ -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)
}

View File

@ -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()
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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/interfaces/04-log-parser/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())
}

View File

@ -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)
}
}

View File

@ -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
}

View File

@ -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)
}
})
}
}

View File

@ -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
}

View File

@ -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},
// })
// }

View File

@ -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)
}
})
}
}

View File

@ -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/interfaces/04-log-parser/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
}