move: advanced funcs to functional programming
This commit is contained in:
20
27-functional-programming/log-parser-exp/field.go
Normal file
20
27-functional-programming/log-parser-exp/field.go
Normal file
@ -0,0 +1,20 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"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 n < 0 || err != nil {
|
||||
f.err = fmt.Errorf("incorrect field -> %q = %q", name, val)
|
||||
}
|
||||
return n
|
||||
}
|
15
27-functional-programming/log-parser-exp/filterby.go
Normal file
15
27-functional-programming/log-parser-exp/filterby.go
Normal file
@ -0,0 +1,15 @@
|
||||
package main
|
||||
|
||||
func filterBy(results []result, filterer filterFunc) []result {
|
||||
out := results[:0]
|
||||
|
||||
for _, r := range results {
|
||||
if !filterer(r) {
|
||||
continue
|
||||
}
|
||||
|
||||
out = append(out, r)
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
34
27-functional-programming/log-parser-exp/filters.go
Normal file
34
27-functional-programming/log-parser-exp/filters.go
Normal file
@ -0,0 +1,34 @@
|
||||
package main
|
||||
|
||||
import "strings"
|
||||
|
||||
func noopFilter(r result) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func not(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")
|
||||
}
|
17
27-functional-programming/log-parser-exp/groupby.go
Normal file
17
27-functional-programming/log-parser-exp/groupby.go
Normal file
@ -0,0 +1,17 @@
|
||||
package main
|
||||
|
||||
func groupBy(results []result, keyer groupFunc) []result {
|
||||
grouped := make(map[string]result, len(results))
|
||||
|
||||
for _, cur := range results {
|
||||
key := keyer(cur)
|
||||
grouped[key] = cur.add(grouped[key])
|
||||
}
|
||||
|
||||
out := results[:0]
|
||||
for _, r := range grouped {
|
||||
out = append(out, r)
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
21
27-functional-programming/log-parser-exp/groupers.go
Normal file
21
27-functional-programming/log-parser-exp/groupers.go
Normal file
@ -0,0 +1,21 @@
|
||||
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
|
||||
}
|
||||
|
||||
// you could have created a noopGrouper as well
|
||||
// but it's not necessary i think (map allocation)
|
||||
func noopGrouper(r result) string {
|
||||
// with something like:
|
||||
// return randomStrings()
|
||||
return ""
|
||||
}
|
16
27-functional-programming/log-parser-exp/log.txt
Normal file
16
27-functional-programming/log-parser-exp/log.txt
Normal file
@ -0,0 +1,16 @@
|
||||
learngoprogramming.com.tr / 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
|
16
27-functional-programming/log-parser-exp/log_err_missing.txt
Normal file
16
27-functional-programming/log-parser-exp/log_err_missing.txt
Normal 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
|
@ -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
|
16
27-functional-programming/log-parser-exp/log_err_str.txt
Normal file
16
27-functional-programming/log-parser-exp/log_err_str.txt
Normal 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
|
49
27-functional-programming/log-parser-exp/main.go
Normal file
49
27-functional-programming/log-parser-exp/main.go
Normal 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"
|
||||
"os"
|
||||
)
|
||||
|
||||
func main() {
|
||||
defer recoverErr()
|
||||
|
||||
_, err := newReport().
|
||||
from(os.Stdin).
|
||||
to(os.Stdout).
|
||||
retrieveFrom(textReader).
|
||||
filterBy(orgDomainsFilter).
|
||||
// filterBy(not(domainExtFilter("org", "io"))).
|
||||
// groupBy(pageGrouper).
|
||||
groupBy(domainGrouper).
|
||||
writeTo(textWriter).
|
||||
run()
|
||||
|
||||
if err != nil {
|
||||
fmt.Println("> Err:", err)
|
||||
}
|
||||
}
|
||||
|
||||
func recoverErr() {
|
||||
val := recover()
|
||||
|
||||
if val == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err, ok := val.(string); ok {
|
||||
fmt.Println("> Error occurred:", err)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
newReport -> stats.NewReport().
|
||||
Result -> stats.Record
|
||||
*/
|
85
27-functional-programming/log-parser-exp/report.go
Normal file
85
27-functional-programming/log-parser-exp/report.go
Normal file
@ -0,0 +1,85 @@
|
||||
package main
|
||||
|
||||
import "io"
|
||||
|
||||
type (
|
||||
parserFunc func(io.Reader) ([]result, error)
|
||||
filterFunc func(result) bool
|
||||
groupFunc func(result) string
|
||||
outputFunc func(io.Writer, []result) error
|
||||
)
|
||||
|
||||
type report struct {
|
||||
input io.Reader
|
||||
output io.Writer
|
||||
parser parserFunc
|
||||
filterer filterFunc
|
||||
grouper groupFunc
|
||||
outputter outputFunc
|
||||
}
|
||||
|
||||
func newReport() *report {
|
||||
return &report{
|
||||
// parser: textParser,
|
||||
filterer: noopFilter,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *report) from(reader io.Reader) *report {
|
||||
r.input = reader
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *report) to(writer io.Writer) *report {
|
||||
r.output = writer
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *report) retrieveFrom(fn parserFunc) *report {
|
||||
r.parser = fn
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *report) filterBy(fn filterFunc) *report {
|
||||
r.filterer = fn
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *report) groupBy(fn groupFunc) *report {
|
||||
r.grouper = fn
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *report) writeTo(fn outputFunc) *report {
|
||||
r.outputter = fn
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *report) run() ([]result, error) {
|
||||
if r.parser == nil {
|
||||
panic("report retriever cannot be nil")
|
||||
}
|
||||
|
||||
results, err := r.parser(r.input)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// noop if filterer is nil
|
||||
results = filterBy(results, r.filterer)
|
||||
|
||||
// grouper is more tricky
|
||||
// you don't want to create an unnecessary map
|
||||
if r.grouper != nil {
|
||||
results = groupBy(results, r.grouper)
|
||||
}
|
||||
|
||||
// prefer: noop output
|
||||
if r.output != nil {
|
||||
if err := r.outputter(r.output, results); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
38
27-functional-programming/log-parser-exp/result.go
Normal file
38
27-functional-programming/log-parser-exp/result.go
Normal file
@ -0,0 +1,38 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// result stores the parsed result for a domain
|
||||
type result struct {
|
||||
domain string
|
||||
page string
|
||||
visits int
|
||||
uniques int
|
||||
}
|
||||
|
||||
// parseLine parses a log line and returns the parsed result with an error
|
||||
func parseLine(line string) (r result, err error) {
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) != 4 {
|
||||
return r, fmt.Errorf("wrong number of fields -> %v", 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
|
||||
}
|
||||
|
||||
// add adds the metrics of another result to the result
|
||||
func (r result) add(other result) result {
|
||||
r.visits += other.visits
|
||||
r.uniques += other.uniques
|
||||
return r
|
||||
}
|
44
27-functional-programming/log-parser-exp/textreader.go
Normal file
44
27-functional-programming/log-parser-exp/textreader.go
Normal 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 (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
func textReader(r io.Reader) ([]result, error) {
|
||||
in := bufio.NewScanner(r)
|
||||
return parseText(in)
|
||||
}
|
||||
|
||||
func parseText(in *bufio.Scanner) ([]result, error) {
|
||||
var (
|
||||
results []result
|
||||
lines int
|
||||
)
|
||||
|
||||
for in.Scan() {
|
||||
lines++
|
||||
|
||||
result, err := parseLine(in.Text())
|
||||
if err != nil {
|
||||
// TODO: custom error type for line information
|
||||
return nil, fmt.Errorf("line %d: %v", lines, err)
|
||||
}
|
||||
|
||||
results = append(results, result)
|
||||
}
|
||||
|
||||
if err := in.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
30
27-functional-programming/log-parser-exp/textwriter.go
Normal file
30
27-functional-programming/log-parser-exp/textwriter.go
Normal file
@ -0,0 +1,30 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// TODO: sort by result key interfaces section
|
||||
|
||||
func textWriter(w io.Writer, results []result) error {
|
||||
fmt.Fprintf(w, "%-25s %-10s %10s %10s\n",
|
||||
"DOMAINS", "PAGES", "VISITS", "UNIQUES")
|
||||
|
||||
fmt.Fprintln(w, strings.Repeat("-", 58))
|
||||
|
||||
var total result
|
||||
|
||||
for _, r := range results {
|
||||
total = total.add(r)
|
||||
|
||||
fmt.Fprintf(w, "%-25s %-10s %10d %10d\n",
|
||||
r.domain, r.page, r.visits, r.uniques)
|
||||
}
|
||||
|
||||
fmt.Fprintf(w, "\n%-36s %10d %10d\n",
|
||||
"", total.visits, total.uniques)
|
||||
|
||||
return nil
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
learngoprogramming.com 10
|
||||
learngoprogramming.com 10
|
||||
golang.org 4
|
||||
golang.org 6
|
||||
blog.golang.org 20
|
||||
blog.golang.org 10
|
@ -1,6 +0,0 @@
|
||||
learngoprogramming.com 10
|
||||
learngoprogramming.com 10
|
||||
golang.org
|
||||
golang.org 6
|
||||
blog.golang.org 20
|
||||
blog.golang.org 10
|
@ -1,6 +0,0 @@
|
||||
learngoprogramming.com 10
|
||||
learngoprogramming.com 10
|
||||
golang.org -100
|
||||
golang.org 6
|
||||
blog.golang.org 20
|
||||
blog.golang.org 10
|
@ -1,6 +0,0 @@
|
||||
learngoprogramming.com 10
|
||||
learngoprogramming.com 10
|
||||
golang.org FOUR
|
||||
golang.org 6
|
||||
blog.golang.org 20
|
||||
blog.golang.org 10
|
@ -1,48 +0,0 @@
|
||||
// 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"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func main() {
|
||||
p := newParser()
|
||||
|
||||
in := bufio.NewScanner(os.Stdin)
|
||||
for in.Scan() {
|
||||
parsed := parse(p, in.Text())
|
||||
update(p, parsed)
|
||||
}
|
||||
|
||||
summarize(p)
|
||||
dumpErrs(in.Err(), err(p))
|
||||
}
|
||||
|
||||
// summarize summarizes and prints the parsing result
|
||||
func summarize(p *parser) {
|
||||
fmt.Printf("%-30s %10s\n", "DOMAIN", "VISITS")
|
||||
fmt.Println(strings.Repeat("-", 45))
|
||||
|
||||
loop(p, func(r result) {
|
||||
fmt.Printf("%-30s %10d\n", r.domain, r.visits)
|
||||
})
|
||||
fmt.Printf("\n%-30s %10d\n", "TOTAL", totalVisits(p))
|
||||
}
|
||||
|
||||
// dumpErrs simplifies handling multiple errors
|
||||
func dumpErrs(errs ...error) {
|
||||
for _, err := range errs {
|
||||
if err != nil {
|
||||
fmt.Println("> Err:", err)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,101 +0,0 @@
|
||||
// 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 the parsed result for a domain
|
||||
type result struct {
|
||||
domain string
|
||||
visits int
|
||||
// add more metrics if 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 constructs, initializes and returns a new parser
|
||||
func newParser() *parser {
|
||||
return &parser{sum: make(map[string]result)}
|
||||
}
|
||||
|
||||
// parse parses a log line and returns the parsed result with an error
|
||||
func parse(p *parser, 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 updates all the parsing results using the given parsing result
|
||||
func update(p *parser, r result) {
|
||||
if p.lerr != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Collect the unique domains
|
||||
if _, ok := p.sum[r.domain]; !ok {
|
||||
p.domains = append(p.domains, r.domain)
|
||||
}
|
||||
|
||||
// Keep track of total and per domain visits
|
||||
p.total += r.visits
|
||||
|
||||
// create and assign a new copy of `visit`
|
||||
p.sum[r.domain] = result{
|
||||
domain: r.domain,
|
||||
visits: r.visits + p.sum[r.domain].visits,
|
||||
}
|
||||
}
|
||||
|
||||
// err returns the last error encountered
|
||||
func err(p *parser) error {
|
||||
return p.lerr
|
||||
}
|
||||
|
||||
// loop allows a func to access all the copies of parsing results
|
||||
func loop(p *parser, feed func(result)) {
|
||||
sort.Strings(p.domains)
|
||||
|
||||
for _, domain := range p.domains {
|
||||
feed(p.sum[domain])
|
||||
}
|
||||
}
|
||||
|
||||
// total returns the total visits
|
||||
func totalVisits(p *parser) int {
|
||||
return p.total
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
# IDEAS
|
||||
|
||||
* Create a chainable funcs for strings
|
||||
* Create a mapper instead of a filter
|
||||
* Create map/reduce
|
||||
* Create a time it takes higher-order func
|
||||
* Use stdlib funcs with func values
|
||||
* Create a functional program with structs (filter etc)
|
Reference in New Issue
Block a user