diff --git a/26-functions-and-pointers/01-basics/bad.go b/25-functions-and-pointers/01-basics/bad.go similarity index 100% rename from 26-functions-and-pointers/01-basics/bad.go rename to 25-functions-and-pointers/01-basics/bad.go diff --git a/26-functions-and-pointers/01-basics/main.go b/25-functions-and-pointers/01-basics/main.go similarity index 100% rename from 26-functions-and-pointers/01-basics/main.go rename to 25-functions-and-pointers/01-basics/main.go diff --git a/26-functions-and-pointers/02-basics/main.go b/25-functions-and-pointers/02-basics/main.go similarity index 100% rename from 26-functions-and-pointers/02-basics/main.go rename to 25-functions-and-pointers/02-basics/main.go diff --git a/26-functions-and-pointers/03-refactor-to-funcs/log.txt b/25-functions-and-pointers/03-refactor-to-funcs/log.txt similarity index 100% rename from 26-functions-and-pointers/03-refactor-to-funcs/log.txt rename to 25-functions-and-pointers/03-refactor-to-funcs/log.txt diff --git a/26-functions-and-pointers/03-refactor-to-funcs/log_err_missing.txt b/25-functions-and-pointers/03-refactor-to-funcs/log_err_missing.txt similarity index 100% rename from 26-functions-and-pointers/03-refactor-to-funcs/log_err_missing.txt rename to 25-functions-and-pointers/03-refactor-to-funcs/log_err_missing.txt diff --git a/26-functions-and-pointers/03-refactor-to-funcs/log_err_negative.txt b/25-functions-and-pointers/03-refactor-to-funcs/log_err_negative.txt similarity index 100% rename from 26-functions-and-pointers/03-refactor-to-funcs/log_err_negative.txt rename to 25-functions-and-pointers/03-refactor-to-funcs/log_err_negative.txt diff --git a/26-functions-and-pointers/03-refactor-to-funcs/log_err_str.txt b/25-functions-and-pointers/03-refactor-to-funcs/log_err_str.txt similarity index 100% rename from 26-functions-and-pointers/03-refactor-to-funcs/log_err_str.txt rename to 25-functions-and-pointers/03-refactor-to-funcs/log_err_str.txt diff --git a/25-functions-and-pointers/03-refactor-to-funcs/main.go b/25-functions-and-pointers/03-refactor-to-funcs/main.go new file mode 100644 index 0000000..a669711 --- /dev/null +++ b/25-functions-and-pointers/03-refactor-to-funcs/main.go @@ -0,0 +1,62 @@ +// 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" + "sort" + "strings" +) + +func main() { + p := newParser() + + // Scan the standard-in line by line + in := bufio.NewScanner(os.Stdin) + for in.Scan() { + p.lines++ + + parsed, err := parse(p, in.Text()) + if err != nil { + fmt.Println(err) + return + } + + domain, visits := parsed.domain, parsed.visits + + // Collect the unique domains + if _, ok := p.sum[domain]; !ok { + p.domains = append(p.domains, domain) + } + + // Keep track of total and per domain visits + p.total += visits + + // create and assign a new copy of `visit` + p.sum[domain] = result{ + domain: domain, + visits: visits + p.sum[domain].visits, + } + } + + // Print the visits per domain + sort.Strings(p.domains) + + fmt.Printf("%-30s %10s\n", "DOMAIN", "VISITS") + fmt.Println(strings.Repeat("-", 45)) + + for _, domain := range p.domains { + parsed := p.sum[domain] + fmt.Printf("%-30s %10d\n", domain, parsed.visits) + } + + // Print the total visits for all domains + fmt.Printf("\n%-30s %10d\n", "TOTAL", p.total) +} diff --git a/25-functions-and-pointers/03-refactor-to-funcs/parser.go b/25-functions-and-pointers/03-refactor-to-funcs/parser.go new file mode 100644 index 0000000..88552bf --- /dev/null +++ b/25-functions-and-pointers/03-refactor-to-funcs/parser.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" +) + +// 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) +} + +// 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) (parsed result, err error) { + fields := strings.Fields(line) + if len(fields) != 2 { + err = fmt.Errorf("wrong input: %v (line #%d)", fields, p.lines) + return + } + + parsed.domain = fields[0] + + parsed.visits, err = strconv.Atoi(fields[1]) + if parsed.visits < 0 || err != nil { + err = fmt.Errorf("wrong input: %q (line #%d)", fields[1], p.lines) + return + } + + return +} diff --git a/26-functions-and-pointers/04-pass-by-value-semantics/log.txt b/25-functions-and-pointers/04-pass-by-value-semantics/log.txt similarity index 100% rename from 26-functions-and-pointers/04-pass-by-value-semantics/log.txt rename to 25-functions-and-pointers/04-pass-by-value-semantics/log.txt diff --git a/26-functions-and-pointers/04-pass-by-value-semantics/log_err_missing.txt b/25-functions-and-pointers/04-pass-by-value-semantics/log_err_missing.txt similarity index 100% rename from 26-functions-and-pointers/04-pass-by-value-semantics/log_err_missing.txt rename to 25-functions-and-pointers/04-pass-by-value-semantics/log_err_missing.txt diff --git a/26-functions-and-pointers/04-pass-by-value-semantics/log_err_negative.txt b/25-functions-and-pointers/04-pass-by-value-semantics/log_err_negative.txt similarity index 100% rename from 26-functions-and-pointers/04-pass-by-value-semantics/log_err_negative.txt rename to 25-functions-and-pointers/04-pass-by-value-semantics/log_err_negative.txt diff --git a/26-functions-and-pointers/04-pass-by-value-semantics/log_err_str.txt b/25-functions-and-pointers/04-pass-by-value-semantics/log_err_str.txt similarity index 100% rename from 26-functions-and-pointers/04-pass-by-value-semantics/log_err_str.txt rename to 25-functions-and-pointers/04-pass-by-value-semantics/log_err_str.txt diff --git a/25-functions-and-pointers/04-pass-by-value-semantics/main.go b/25-functions-and-pointers/04-pass-by-value-semantics/main.go new file mode 100644 index 0000000..d63490d --- /dev/null +++ b/25-functions-and-pointers/04-pass-by-value-semantics/main.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" + "sort" + "strings" +) + +func main() { + p := newParser() + + // Scan the standard-in line by line + in := bufio.NewScanner(os.Stdin) + for in.Scan() { + p.lines++ + + parsed, err := parse(p, in.Text()) + if err != nil { + fmt.Println(err) + break + } + + p = update(p, parsed) + } + + // Print the visits per domain + sort.Strings(p.domains) + + fmt.Printf("%-30s %10s\n", "DOMAIN", "VISITS") + fmt.Println(strings.Repeat("-", 45)) + + for _, domain := range p.domains { + parsed := p.sum[domain] + fmt.Printf("%-30s %10d\n", domain, parsed.visits) + } + + // Print the total visits for all domains + fmt.Printf("\n%-30s %10d\n", "TOTAL", p.total) +} diff --git a/25-functions-and-pointers/04-pass-by-value-semantics/parse.go b/25-functions-and-pointers/04-pass-by-value-semantics/parse.go new file mode 100644 index 0000000..53a4696 --- /dev/null +++ b/25-functions-and-pointers/04-pass-by-value-semantics/parse.go @@ -0,0 +1,71 @@ +// 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" +) + +// result stores metrics 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) +} + +func newParser() parser { + return parser{sum: make(map[string]result)} +} + +func parse(p parser, line string) (parsed result, err error) { + fields := strings.Fields(line) + if len(fields) != 2 { + err = fmt.Errorf("wrong input: %v (line #%d)", fields, p.lines) + return + } + + parsed.domain = fields[0] + + parsed.visits, err = strconv.Atoi(fields[1]) + if parsed.visits < 0 || err != nil { + err = fmt.Errorf("wrong input: %q (line #%d)", fields[1], p.lines) + return + } + + return +} + +func update(p parser, parsed result) parser { + domain, visits := parsed.domain, parsed.visits + + // Collect the unique domains + if _, ok := p.sum[domain]; !ok { + p.domains = append(p.domains, domain) + } + + // Keep track of total and per domain visits + p.total += visits + + // create and assign a new copy of `visit` + p.sum[domain] = result{ + domain: domain, + visits: visits + p.sum[domain].visits, + } + + return p +} diff --git a/26-functions-and-pointers/05-pointers/main.go b/25-functions-and-pointers/05-pointers/main.go similarity index 100% rename from 26-functions-and-pointers/05-pointers/main.go rename to 25-functions-and-pointers/05-pointers/main.go diff --git a/26-functions-and-pointers/06-pointers-basic-examples/main.go b/25-functions-and-pointers/06-pointers-basic-examples/main.go similarity index 100% rename from 26-functions-and-pointers/06-pointers-basic-examples/main.go rename to 25-functions-and-pointers/06-pointers-basic-examples/main.go diff --git a/26-functions-and-pointers/07-pointers-composites/main.go b/25-functions-and-pointers/07-pointers-composites/main.go similarity index 100% rename from 26-functions-and-pointers/07-pointers-composites/main.go rename to 25-functions-and-pointers/07-pointers-composites/main.go diff --git a/26-functions-and-pointers/08-log-parser-pointers/log.txt b/25-functions-and-pointers/08-log-parser-pointers/log.txt similarity index 100% rename from 26-functions-and-pointers/08-log-parser-pointers/log.txt rename to 25-functions-and-pointers/08-log-parser-pointers/log.txt diff --git a/26-functions-and-pointers/08-log-parser-pointers/log_err_missing.txt b/25-functions-and-pointers/08-log-parser-pointers/log_err_missing.txt similarity index 100% rename from 26-functions-and-pointers/08-log-parser-pointers/log_err_missing.txt rename to 25-functions-and-pointers/08-log-parser-pointers/log_err_missing.txt diff --git a/26-functions-and-pointers/08-log-parser-pointers/log_err_negative.txt b/25-functions-and-pointers/08-log-parser-pointers/log_err_negative.txt similarity index 100% rename from 26-functions-and-pointers/08-log-parser-pointers/log_err_negative.txt rename to 25-functions-and-pointers/08-log-parser-pointers/log_err_negative.txt diff --git a/26-functions-and-pointers/08-log-parser-pointers/log_err_str.txt b/25-functions-and-pointers/08-log-parser-pointers/log_err_str.txt similarity index 100% rename from 26-functions-and-pointers/08-log-parser-pointers/log_err_str.txt rename to 25-functions-and-pointers/08-log-parser-pointers/log_err_str.txt diff --git a/26-functions-and-pointers/04-pass-by-value-semantics/main.go b/25-functions-and-pointers/08-log-parser-pointers/main.go similarity index 64% rename from 26-functions-and-pointers/04-pass-by-value-semantics/main.go rename to 25-functions-and-pointers/08-log-parser-pointers/main.go index ebfe963..f24d393 100644 --- a/26-functions-and-pointers/04-pass-by-value-semantics/main.go +++ b/25-functions-and-pointers/08-log-parser-pointers/main.go @@ -19,21 +19,18 @@ func main() { p := newParser() for in.Scan() { - p.lines++ - - d, err := parse(p, in.Text()) - if err != nil { - fmt.Println(err) - return - } - - p = push(p, d) + add(p, in.Text()) } - for _, d := range p.domains { - vis := p.sum[d.name] + for _, name := range p.domains { + // vis := p.sum[d.name] + d := p.sum[name] - fmt.Printf("%-25s -> %d\n", d.name, vis) + fmt.Printf("%-25s -> %d\n", d.name, d.visits) } fmt.Printf("\n%-25s -> %d\n", "TOTAL", p.total) + + if p.lerr != nil { + fmt.Printf("> Err: %s\n", p.lerr) + } } diff --git a/25-functions-and-pointers/08-log-parser-pointers/parser.go b/25-functions-and-pointers/08-log-parser-pointers/parser.go new file mode 100644 index 0000000..45b270d --- /dev/null +++ b/25-functions-and-pointers/08-log-parser-pointers/parser.go @@ -0,0 +1,99 @@ +package main + +import ( + "fmt" + "strconv" + "strings" +) + +// TODO: add add() func +// TODO: add error handling (variadics) +// TODO: add iterator func values +// TODO: add summarizer to main() + +// domain represents a single domain log record +type domain struct { + name string + visits int +} + +// parser parses a log file and provides an iterator to iterate upon the domains +// +// the parser struct is carefully crafted to be usable using its zero values except the map field +type parser struct { + // sum map[string]int // visits per unique domain + // domains []domain // unique domain names + sum map[string]domain // visits per unique domain + domains []string // unique domain names + + total int // total visits to all domains + lines int // number of parsed lines (for the error messages) + lerr error // saves the last error occurred +} + +// newParser creates and returns a new parser. +func newParser() *parser { + // return &parser{sum: make(map[string]int)} + return &parser{sum: make(map[string]domain)} +} + +// add parses the given line and saves the result to the internal list of +// domains. it doesn't add the record when the parsing fails. +func add(p *parser, line string) { + // if there was a previous error do not add + if p.lerr != nil { + return + } + + dom, err := parse(p, line) + + // store only the last error + if err != nil { + p.lerr = err + return + } + + push(p, dom) +} + +// parse parses the given text and returns a domain struct +func parse(p *parser, line string) (dom domain, err error) { + p.lines++ + + fields := strings.Fields(line) + if len(fields) != 2 { + err = fmt.Errorf("wrong input: %v (line #%d)", fields, p.lines) + return + } + + dom.name = fields[0] + + dom.visits, err = strconv.Atoi(fields[1]) + if dom.visits < 0 || err != nil { + err = fmt.Errorf("wrong input: %q (line #%d)", fields[1], p.lines) + } + + return +} + +// push pushes the given domain to the internal list of domains. +// it also increases the total visits for all the domains. +func push(p *parser, d domain) { + // TODO: + // if _, ok := p.sum[d.name]; !ok { + // p.domains = append(p.domains, d) + // } + + // p.sum[d.name] += d.visits + // p.total += d.visits + name := d.name + + // collect the unique domains + if _, ok := p.sum[name]; !ok { + p.domains = append(p.domains, name) + } + + p.total += d.visits + d.visits += p.sum[name].visits + p.sum[name] = d +} diff --git a/26-functions-and-pointers/08x-log-parser-pointers/log.txt b/25-functions-and-pointers/08x-log-parser-pointers/log.txt similarity index 100% rename from 26-functions-and-pointers/08x-log-parser-pointers/log.txt rename to 25-functions-and-pointers/08x-log-parser-pointers/log.txt diff --git a/26-functions-and-pointers/08x-log-parser-pointers/log_err_missing.txt b/25-functions-and-pointers/08x-log-parser-pointers/log_err_missing.txt similarity index 100% rename from 26-functions-and-pointers/08x-log-parser-pointers/log_err_missing.txt rename to 25-functions-and-pointers/08x-log-parser-pointers/log_err_missing.txt diff --git a/26-functions-and-pointers/08x-log-parser-pointers/log_err_negative.txt b/25-functions-and-pointers/08x-log-parser-pointers/log_err_negative.txt similarity index 100% rename from 26-functions-and-pointers/08x-log-parser-pointers/log_err_negative.txt rename to 25-functions-and-pointers/08x-log-parser-pointers/log_err_negative.txt diff --git a/26-functions-and-pointers/08x-log-parser-pointers/log_err_str.txt b/25-functions-and-pointers/08x-log-parser-pointers/log_err_str.txt similarity index 100% rename from 26-functions-and-pointers/08x-log-parser-pointers/log_err_str.txt rename to 25-functions-and-pointers/08x-log-parser-pointers/log_err_str.txt diff --git a/25-functions-and-pointers/08x-log-parser-pointers/main.go b/25-functions-and-pointers/08x-log-parser-pointers/main.go new file mode 100644 index 0000000..13247a6 --- /dev/null +++ b/25-functions-and-pointers/08x-log-parser-pointers/main.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" +) + +func main() { + in := bufio.NewScanner(os.Stdin) + + p := newParser() + for in.Scan() { + // dom, err := parse(p, in.Text()) + // if err != nil { + // fmt.Println(err) + // return + // } + + // p = push(p, dom) + + add(p, in.Text()) + } + + summarize(p) + dumpErrs(in.Err(), err(p)) +} + +// funcs not always need to be reused. +// here, it tells about what it does: it summarizes the parsing result. +func summarize(p *parser) { + // multiple iterators can be created. each one remembers the last + // read domain record. + + next, cur := iterator(p) + for next() { + dom := cur() + fmt.Printf("%-25s -> %d\n", dom.name, dom.visits) + } + fmt.Printf("\n%-25s -> %d\n", "TOTAL", p.total) +} + +// this variadic func simplifies the multiple error handling +func dumpErrs(errs ...error) { + for _, err := range errs { + if err != nil { + fmt.Printf("> Err: %s\n", err) + } + } +} diff --git a/25-functions-and-pointers/08x-log-parser-pointers/parser.go b/25-functions-and-pointers/08x-log-parser-pointers/parser.go new file mode 100644 index 0000000..05d9e3b --- /dev/null +++ b/25-functions-and-pointers/08x-log-parser-pointers/parser.go @@ -0,0 +1,110 @@ +package main + +import ( + "fmt" + "strconv" + "strings" +) + +// domain represents a domain log record +type domain struct { + name string + visits int +} + +// parser parses a log file and provides an iterator to iterate upon the domains +// +// the parser struct is carefully crafted to be usable using its zero values except the map field +type parser struct { + sum map[string]domain // visits per unique domain + domains []string // unique domain names + total int // total visits to all domains + lines int // number of parsed lines (for the error messages) + lerr error // saves the last error occurred +} + +// newParser creates and returns a new parser. +func newParser() *parser { + return &parser{sum: make(map[string]domain)} +} + +// add parses the given line and saves the result to the internal list of +// domains. it doesn't add the record when the parsing fails. +func add(p *parser, line string) { + // if there was a previous error do not add + if p.lerr != nil { + return + } + + dom, err := parse(p, line) + + // store only the last error + if err != nil { + p.lerr = err + return + } + + push(p, dom) +} + +// iterator returns two functions for iterating over domains. +// next = returns true when there are more domains to iterate on. +// cur = returns the current domain +func iterator(p *parser) (next func() bool, cur func() domain) { + // remember the last received line + var last int + + next = func() bool { + defer func() { last++ }() + return len(p.domains) > last + } + + cur = func() domain { + // return a copy so the caller cannot change it + name := p.domains[last-1] + return p.sum[name] + } + + return +} + +// error returns the last error occurred +func err(p *parser) error { + return p.lerr +} + +// parse parses the given text and returns a domain struct +func parse(p *parser, line string) (dom domain, err error) { + p.lines++ // increase the parsed line counter (only write is here) + + fields := strings.Fields(line) + if len(fields) != 2 { + err = fmt.Errorf("wrong input: %v (line #%d)", fields, p.lines) + return + } + + name, visits := fields[0], fields[1] + + n, err := strconv.Atoi(visits) + if n < 0 || err != nil { + err = fmt.Errorf("wrong input: %q (line #%d)", visits, p.lines) + return + } + + return domain{name: name, visits: n}, nil +} + +// push pushes the given domain to the internal list of domains. +// it also increases the total visits for all the domains. +func push(p *parser, d domain) { + name := d.name + + // collect the unique domains + if _, ok := p.sum[name]; !ok { + p.domains = append(p.domains, name) + } + + p.total += d.visits + d.visits += p.sum[name].visits + p.sum[name] = d +} diff --git a/26-functions-and-pointers/README.md b/25-functions-and-pointers/README.md similarity index 100% rename from 26-functions-and-pointers/README.md rename to 25-functions-and-pointers/README.md diff --git a/x-tba/2-methods/xxx-log-parser-methods/log.txt b/25-functions-and-pointers/_WIP/log.txt similarity index 100% rename from x-tba/2-methods/xxx-log-parser-methods/log.txt rename to 25-functions-and-pointers/_WIP/log.txt diff --git a/x-tba/2-methods/xxx-log-parser-methods/log_err_missing.txt b/25-functions-and-pointers/_WIP/log_err_missing.txt similarity index 100% rename from x-tba/2-methods/xxx-log-parser-methods/log_err_missing.txt rename to 25-functions-and-pointers/_WIP/log_err_missing.txt diff --git a/x-tba/2-methods/xxx-log-parser-methods/log_err_negative.txt b/25-functions-and-pointers/_WIP/log_err_negative.txt similarity index 100% rename from x-tba/2-methods/xxx-log-parser-methods/log_err_negative.txt rename to 25-functions-and-pointers/_WIP/log_err_negative.txt diff --git a/x-tba/2-methods/xxx-log-parser-methods/log_err_str.txt b/25-functions-and-pointers/_WIP/log_err_str.txt similarity index 100% rename from x-tba/2-methods/xxx-log-parser-methods/log_err_str.txt rename to 25-functions-and-pointers/_WIP/log_err_str.txt diff --git a/26-functions-and-pointers/03-refactor-to-funcs/main.go b/26-functions-and-pointers/03-refactor-to-funcs/main.go deleted file mode 100644 index 3d07b07..0000000 --- a/26-functions-and-pointers/03-refactor-to-funcs/main.go +++ /dev/null @@ -1,44 +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" -) - -func main() { - in := bufio.NewScanner(os.Stdin) - - p := newParser() - - for in.Scan() { - p.lines++ - - d, err := parse(p, in.Text()) - if err != nil { - fmt.Println(err) - return - } - - if _, ok := p.sum[d.name]; !ok { - p.domains = append(p.domains, d) - } - - p.sum[d.name] += d.visits - p.total += d.visits - } - - for _, d := range p.domains { - vis := p.sum[d.name] - - fmt.Printf("%-25s -> %d\n", d.name, vis) - } - fmt.Printf("\n%-25s -> %d\n", "TOTAL", p.total) -} diff --git a/26-functions-and-pointers/03-refactor-to-funcs/parser.go b/26-functions-and-pointers/03-refactor-to-funcs/parser.go deleted file mode 100644 index a1417f5..0000000 --- a/26-functions-and-pointers/03-refactor-to-funcs/parser.go +++ /dev/null @@ -1,49 +0,0 @@ -package main - -import ( - "fmt" - "strconv" - "strings" -) - -// domain represents a domain log record -type domain struct { - name string - visits int -} - -// parser parses a log file and provides an iterator to iterate upon the domains -// -// the parser struct is carefully crafted to be usable using its zero values except the map field -type parser struct { - sum map[string]int // visits per unique domain - domains []domain // unique domain names - total int // total visits to all domains - lines int // number of parsed lines (for the error messages) -} - -// newParser creates and returns a new parser. -func newParser() parser { - return parser{sum: make(map[string]int)} -} - -// parse parses the given text and returns a domain struct -func parse(p parser, line string) (dom domain, err error) { - // var dom domain - // var err error - - fields := strings.Fields(line) - if len(fields) != 2 { - err = fmt.Errorf("wrong input: %v (line #%d)", fields, p.lines) - return - } - name, visits := fields[0], fields[1] - - n, err := strconv.Atoi(visits) - if n < 0 || err != nil { - err = fmt.Errorf("wrong input: %q (line #%d)", visits, p.lines) - return - } - - return domain{name: name, visits: n}, nil -} diff --git a/26-functions-and-pointers/04-pass-by-value-semantics/parser.go b/26-functions-and-pointers/04-pass-by-value-semantics/parser.go deleted file mode 100644 index 462d9f8..0000000 --- a/26-functions-and-pointers/04-pass-by-value-semantics/parser.go +++ /dev/null @@ -1,59 +0,0 @@ -package main - -import ( - "fmt" - "strconv" - "strings" -) - -// domain represents a domain log record -type domain struct { - name string - visits int -} - -// parser parses a log file and provides an iterator to iterate upon the domains -// -// the parser struct is carefully crafted to be usable using its zero values except the map field -type parser struct { - sum map[string]int // visits per unique domain - domains []domain // unique domain names - total int // total visits to all domains - lines int // number of parsed lines (for the error messages) -} - -// newParser creates and returns a new parser. -func newParser() parser { - return parser{sum: make(map[string]int)} -} - -// parse parses the given text and returns a domain struct -func parse(p parser, line string) (dom domain, err error) { - fields := strings.Fields(line) - if len(fields) != 2 { - err = fmt.Errorf("wrong input: %v (line #%d)", fields, p.lines) - return - } - name, visits := fields[0], fields[1] - - n, err := strconv.Atoi(visits) - if n < 0 || err != nil { - err = fmt.Errorf("wrong input: %q (line #%d)", visits, p.lines) - return - } - - return domain{name: name, visits: n}, nil -} - -// push pushes the given domain to the internal list of domains. -// it also increases the total visits for all the domains. -func push(p parser, d domain) parser { - if _, ok := p.sum[d.name]; !ok { - p.domains = append(p.domains, d) - } - - p.sum[d.name] += d.visits - p.total += d.visits - - return p -} diff --git a/x-tba/2-methods/README.md b/26-methods/README.md similarity index 100% rename from x-tba/2-methods/README.md rename to 26-methods/README.md diff --git a/x-tba/2-methods/xxx-log-parser-methods/packaged/log.txt b/26-methods/xxx-log-parser-methods/log.txt similarity index 100% rename from x-tba/2-methods/xxx-log-parser-methods/packaged/log.txt rename to 26-methods/xxx-log-parser-methods/log.txt diff --git a/x-tba/2-methods/xxx-log-parser-methods/packaged/log_err_missing.txt b/26-methods/xxx-log-parser-methods/log_err_missing.txt similarity index 100% rename from x-tba/2-methods/xxx-log-parser-methods/packaged/log_err_missing.txt rename to 26-methods/xxx-log-parser-methods/log_err_missing.txt diff --git a/x-tba/2-methods/xxx-log-parser-methods/packaged/log_err_negative.txt b/26-methods/xxx-log-parser-methods/log_err_negative.txt similarity index 100% rename from x-tba/2-methods/xxx-log-parser-methods/packaged/log_err_negative.txt rename to 26-methods/xxx-log-parser-methods/log_err_negative.txt diff --git a/x-tba/2-methods/xxx-log-parser-methods/packaged/log_err_str.txt b/26-methods/xxx-log-parser-methods/log_err_str.txt similarity index 100% rename from x-tba/2-methods/xxx-log-parser-methods/packaged/log_err_str.txt rename to 26-methods/xxx-log-parser-methods/log_err_str.txt diff --git a/x-tba/2-methods/xxx-log-parser-methods/main.go b/26-methods/xxx-log-parser-methods/main.go similarity index 100% rename from x-tba/2-methods/xxx-log-parser-methods/main.go rename to 26-methods/xxx-log-parser-methods/main.go diff --git a/26-methods/xxx-log-parser-methods/packaged/log.txt b/26-methods/xxx-log-parser-methods/packaged/log.txt new file mode 100644 index 0000000..b449a83 --- /dev/null +++ b/26-methods/xxx-log-parser-methods/packaged/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/26-methods/xxx-log-parser-methods/packaged/log_err_missing.txt b/26-methods/xxx-log-parser-methods/packaged/log_err_missing.txt new file mode 100644 index 0000000..8fbe528 --- /dev/null +++ b/26-methods/xxx-log-parser-methods/packaged/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/26-methods/xxx-log-parser-methods/packaged/log_err_negative.txt b/26-methods/xxx-log-parser-methods/packaged/log_err_negative.txt new file mode 100644 index 0000000..d887144 --- /dev/null +++ b/26-methods/xxx-log-parser-methods/packaged/log_err_negative.txt @@ -0,0 +1,6 @@ +learngoprogramming.com 10 200 +learngoprogramming.com 10 -500 +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/26-methods/xxx-log-parser-methods/packaged/log_err_str.txt b/26-methods/xxx-log-parser-methods/packaged/log_err_str.txt new file mode 100644 index 0000000..4ccb676 --- /dev/null +++ b/26-methods/xxx-log-parser-methods/packaged/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/x-tba/2-methods/xxx-log-parser-methods/packaged/main.go b/26-methods/xxx-log-parser-methods/packaged/main.go similarity index 100% rename from x-tba/2-methods/xxx-log-parser-methods/packaged/main.go rename to 26-methods/xxx-log-parser-methods/packaged/main.go diff --git a/x-tba/2-methods/xxx-log-parser-methods/packaged/metrics/parser.go b/26-methods/xxx-log-parser-methods/packaged/metrics/parser.go similarity index 95% rename from x-tba/2-methods/xxx-log-parser-methods/packaged/metrics/parser.go rename to 26-methods/xxx-log-parser-methods/packaged/metrics/parser.go index d6ff9b0..0393505 100644 --- a/x-tba/2-methods/xxx-log-parser-methods/packaged/metrics/parser.go +++ b/26-methods/xxx-log-parser-methods/packaged/metrics/parser.go @@ -17,7 +17,7 @@ type Parser struct { lerr error // the last error occurred } -// Parsed wraps a result for generating parser error +// Parsed wraps a result for generating a parser error type Parsed struct { result // use struct embedding err error // inject an error diff --git a/x-tba/2-methods/xxx-log-parser-methods/packaged/metrics/report.go b/26-methods/xxx-log-parser-methods/packaged/metrics/report.go similarity index 90% rename from x-tba/2-methods/xxx-log-parser-methods/packaged/metrics/report.go rename to 26-methods/xxx-log-parser-methods/packaged/metrics/report.go index ecf1060..87fea02 100644 --- a/x-tba/2-methods/xxx-log-parser-methods/packaged/metrics/report.go +++ b/26-methods/xxx-log-parser-methods/packaged/metrics/report.go @@ -20,18 +20,19 @@ type Report struct { } // NewReport constructs and initializes a new report +// You can't use its methods without pointer mechanics func NewReport() *Report { return &Report{sum: make(map[string]result)} } // Update updates the report for the given parsing result -func (r *Report) Update(parsed Parsed) { +func (r *Report) Update(p Parsed) { // do not update the report if the result has an error - if parsed.err != nil { + if p.err != nil { return } - domain := parsed.Domain + domain := p.Domain if _, ok := r.sum[domain]; !ok { r.domains = append(r.domains, domain) } @@ -39,8 +40,8 @@ func (r *Report) Update(parsed Parsed) { // let the result handle the addition // this allows us to manage the result in once place // and this way it becomes easily extendable - r.total = r.total.add(parsed.result) - r.sum[domain] = parsed.add(r.sum[domain]) + r.total = r.total.add(p.result) + r.sum[domain] = p.add(r.sum[domain]) } // Iterator returns `next()` to detect when the iteration ends, diff --git a/x-tba/2-methods/xxx-log-parser-methods/packaged/metrics/result.go b/26-methods/xxx-log-parser-methods/packaged/metrics/result.go similarity index 64% rename from x-tba/2-methods/xxx-log-parser-methods/packaged/metrics/result.go rename to 26-methods/xxx-log-parser-methods/packaged/metrics/result.go index 7da44eb..5295fd0 100644 --- a/x-tba/2-methods/xxx-log-parser-methods/packaged/metrics/result.go +++ b/26-methods/xxx-log-parser-methods/packaged/metrics/result.go @@ -19,33 +19,40 @@ import ( // it uses the value mechanics, // because it doesn't have to update anything type result struct { - Domain string - Visits int + Domain string + Visits int + TimeSpent int // 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, + Domain: r.Domain, + Visits: r.Visits + other.Visits, + TimeSpent: r.TimeSpent + other.TimeSpent, } } // parse parses a single log line -func parse(line string) (parsed result, err error) { +func parse(line string) (r result, err error) { fields := strings.Fields(line) - if len(fields) != 2 { + if len(fields) != 3 { err = fmt.Errorf("wrong input: %v", fields) return } - parsed.Domain = fields[0] + r.Domain = fields[0] - parsed.Visits, err = strconv.Atoi(fields[1]) - if parsed.Visits < 0 || err != nil { + r.Visits, err = strconv.Atoi(fields[1]) + if r.Visits < 0 || err != nil { err = fmt.Errorf("wrong input: %q", fields[1]) } + r.TimeSpent, err = strconv.Atoi(fields[2]) + if r.TimeSpent < 0 || err != nil { + err = fmt.Errorf("wrong input: %q", fields[2]) + } + return } diff --git a/x-tba/2-methods/xxx-log-parser-methods/packaged/summarize.go b/26-methods/xxx-log-parser-methods/packaged/summarize.go similarity index 67% rename from x-tba/2-methods/xxx-log-parser-methods/packaged/summarize.go rename to 26-methods/xxx-log-parser-methods/packaged/summarize.go index 2aaeb6d..34bc72c 100644 --- a/x-tba/2-methods/xxx-log-parser-methods/packaged/summarize.go +++ b/26-methods/xxx-log-parser-methods/packaged/summarize.go @@ -16,15 +16,23 @@ import ( // summarize prints the report and errors if any func summarize(rep *metrics.Report, errs ...error) { - fmt.Printf("%-30s %10s\n", "DOMAIN", "VISITS") - fmt.Println(strings.Repeat("-", 45)) + // TODO: make it strings.Builder + + const format = "%-30s %10s %20s\n" + const formatValue = "%-30s %10d %20d\n" + + fmt.Printf(format, "DOMAIN", "VISITS", "TIME SPENT") + fmt.Println(strings.Repeat("-", 65)) next, cur := rep.Iterator() for next() { rec := cur() - fmt.Printf("%-30s %10d\n", rec.Domain, rec.Visits) + fmt.Printf(formatValue, rec.Domain, rec.Visits, rec.TimeSpent) } - fmt.Printf("\n%-30s %10d\n", "TOTAL", rep.Total().Visits) + + fmt.Printf("\n"+formatValue, "TOTAL", + rep.Total().Visits, rep.Total().TimeSpent, + ) // only handle the errors once dumpErrs(errs...) diff --git a/x-tba/2-methods/xxx-log-parser-methods/parser.go b/26-methods/xxx-log-parser-methods/parser.go similarity index 100% rename from x-tba/2-methods/xxx-log-parser-methods/parser.go rename to 26-methods/xxx-log-parser-methods/parser.go diff --git a/x-tba/2-methods/xxx-log-parser-methods/report.go b/26-methods/xxx-log-parser-methods/report.go similarity index 89% rename from x-tba/2-methods/xxx-log-parser-methods/report.go rename to 26-methods/xxx-log-parser-methods/report.go index 151267a..78cc216 100644 --- a/x-tba/2-methods/xxx-log-parser-methods/report.go +++ b/26-methods/xxx-log-parser-methods/report.go @@ -22,13 +22,13 @@ func newReport() *report { } // update updates the errors for the given parsing result -func (r *report) update(parsed parsed) { +func (r *report) update(p parsed) { // do not update the report if the result has an error - if parsed.err != nil { + if p.err != nil { return } - domain := parsed.domain + domain := p.domain if _, ok := r.sum[domain]; !ok { r.domains = append(r.domains, domain) } @@ -36,8 +36,8 @@ func (r *report) update(parsed parsed) { // let the result handle the addition // this allows us to manage the result in once place // and this way it becomes easily extendable - r.total = r.total.add(parsed.result) - r.sum[domain] = parsed.add(r.sum[domain]) + r.total = r.total.add(p.result) + r.sum[domain] = p.add(r.sum[domain]) } // iterator returns `next()` to detect when the iteration ends, diff --git a/x-tba/2-methods/xxx-log-parser-methods/result.go b/26-methods/xxx-log-parser-methods/result.go similarity index 84% rename from x-tba/2-methods/xxx-log-parser-methods/result.go rename to 26-methods/xxx-log-parser-methods/result.go index 097e22f..3f622de 100644 --- a/x-tba/2-methods/xxx-log-parser-methods/result.go +++ b/26-methods/xxx-log-parser-methods/result.go @@ -33,17 +33,17 @@ func (r result) add(other result) result { } // parseLine parses a single result line -func parseLine(line string) (parsed result, err error) { +func parseLine(line string) (p result, err error) { fields := strings.Fields(line) if len(fields) != 2 { err = fmt.Errorf("wrong input: %v", fields) return } - parsed.domain = fields[0] + p.domain = fields[0] - parsed.visits, err = strconv.Atoi(fields[1]) - if parsed.visits < 0 || err != nil { + p.visits, err = strconv.Atoi(fields[1]) + if p.visits < 0 || err != nil { err = fmt.Errorf("wrong input: %q", fields[1]) } diff --git a/x-tba/2-methods/xxx-log-parser-methods/summarize.go b/26-methods/xxx-log-parser-methods/summarize.go similarity index 100% rename from x-tba/2-methods/xxx-log-parser-methods/summarize.go rename to 26-methods/xxx-log-parser-methods/summarize.go diff --git a/x-tba/3-interfaces/README.md b/27-interfaces/README.md similarity index 100% rename from x-tba/3-interfaces/README.md rename to 27-interfaces/README.md diff --git a/x-tba/4-concurrency/README.md b/28-concurrency/README.md similarity index 100% rename from x-tba/4-concurrency/README.md rename to 28-concurrency/README.md diff --git a/28-concurrency/xxx-concurrent-downloader/fetch/danger/main.go b/28-concurrency/xxx-concurrent-downloader/fetch/danger/main.go new file mode 100644 index 0000000..acca60b --- /dev/null +++ b/28-concurrency/xxx-concurrent-downloader/fetch/danger/main.go @@ -0,0 +1,46 @@ +package main + +import ( + "fmt" + "time" +) + +/* +when the default case exists, messages are not guaranteed +*/ +func main() { + ch := make(chan int) + + // if this GR is fast enough + // the main GR can miss the signals + go func() { + var i int + + for { + i++ + + select { + case ch <- i: + default: + // message is lost + // it doesn't matter whether the chan is buffered or not + } + + if i == 10000 { + fmt.Println("gopher dies") + close(ch) + return + } + } + }() + + var total int + for i := range ch { + // works slower — misses some of the signals + time.Sleep(time.Nanosecond) + total += i + } + + // should be: 50005000 + fmt.Println(total) +} diff --git a/28-concurrency/xxx-concurrent-downloader/fetch/danger2/main.go b/28-concurrency/xxx-concurrent-downloader/fetch/danger2/main.go new file mode 100644 index 0000000..c937201 --- /dev/null +++ b/28-concurrency/xxx-concurrent-downloader/fetch/danger2/main.go @@ -0,0 +1,40 @@ +package main + +import ( + "fmt" + "time" +) + +func main() { + a, b := make(chan bool), make(chan bool) + + go func() { + for a != nil || b != nil { + fmt.Println("loop starts") + + select { + case <-a: + fmt.Println("recv: a") + a = nil + case <-b: + b = nil + fmt.Println("recv: b") + } + + fmt.Println("loop ends") + } + fmt.Println("gopher dies") + }() + + time.Sleep(time.Second) + // a <- true + close(a) + time.Sleep(time.Second) + //b <- true + close(b) + time.Sleep(time.Second * 2) + + // closed chan never blocks + // nil chan always blocks + // if in the loop chans not set to nil, the loop will loop forever +} diff --git a/28-concurrency/xxx-concurrent-downloader/fetch/drain.go b/28-concurrency/xxx-concurrent-downloader/fetch/drain.go new file mode 100644 index 0000000..98346b4 --- /dev/null +++ b/28-concurrency/xxx-concurrent-downloader/fetch/drain.go @@ -0,0 +1,12 @@ +package fetch + +// Drain drains the progress updates and returns the latest progresses +func Drain(updates <-chan Progress) map[string]Progress { + latest := make(map[string]Progress) + + // save the latest progress + for p := range updates { + latest[p.URL] = p + } + return latest +} diff --git a/28-concurrency/xxx-concurrent-downloader/fetch/httpget.go b/28-concurrency/xxx-concurrent-downloader/fetch/httpget.go new file mode 100644 index 0000000..56104fa --- /dev/null +++ b/28-concurrency/xxx-concurrent-downloader/fetch/httpget.go @@ -0,0 +1,31 @@ +package fetch + +import ( + "fmt" + "net/http" + "time" +) + +// HTTPGet requests to a url with the specified timeout. +// And it returns the response with an error. +// The caller should drain and close the body or it will leak. +func HTTPGet(url string, timeout time.Duration) (*http.Response, error) { + req, err := http.NewRequest("GET", url, nil) + req.Close = true + if err != nil { + return nil, err + } + + c := &http.Client{Timeout: timeout} + resp, err := c.Do(req) + if err != nil { + return nil, err + } + + // checkout for the bad urls + if s := resp.StatusCode; s < 200 || s > 299 { + return resp, fmt.Errorf("bad status: %d", s) + } + + return resp, nil +} diff --git a/28-concurrency/xxx-concurrent-downloader/fetch/httptransfer.go b/28-concurrency/xxx-concurrent-downloader/fetch/httptransfer.go new file mode 100644 index 0000000..1b132d0 --- /dev/null +++ b/28-concurrency/xxx-concurrent-downloader/fetch/httptransfer.go @@ -0,0 +1,126 @@ +package fetch + +import ( + "fmt" + "io" + "net/http" + "os" + "path" + "time" +) + +// HTTPTransfer uses a fetcher and a storager to fetch and store an url +type HTTPTransfer struct { + Progress + r io.ReadCloser + w io.WriteCloser + + timeout time.Duration + + done <-chan bool + updates chan<- Progress +} + +// NewHTTPTransfer creates and returns a new transfer +func NewHTTPTransfer(url string, timeout time.Duration) *HTTPTransfer { + return &HTTPTransfer{ + Progress: Progress{URL: url}, + timeout: timeout, + } +} + +// Start starts the transfer progress +// The transfer will send its updates to the update chan +// and it will stop when receives a signal from the done chan +// +// All the t.signal calls are here. I believe this increases the visibility +// of the Start function (what it does). If the signal calls were in the +// other funcs of HTTPTransfer, it could easily lead to bugs. +func (t *HTTPTransfer) Start(updates chan<- Progress, done <-chan bool) { + defer t.cleanup() + + t.done, t.updates = done, updates + + t.w, t.Error = os.Create(path.Base(t.URL)) + if t.Error != nil { + t.signal() + return + } + + t.request() + if !t.signal() { + return + } + + // sniff the on-going transfer until the signal returns false + sniff := sniffer(func(p []byte) bool { + l := len(p) + t.Current = l + t.Downloaded += l + + return t.signal() + }) + + t.transfer(sniff) + t.signal() +} + +func (t *HTTPTransfer) cleanup() { + if t.r != nil { + t.r.Close() + } + if t.w != nil { + t.w.Close() + } +} + +func (t *HTTPTransfer) request() { + var resp *http.Response + + resp, t.Error = HTTPGet(t.URL, t.timeout) + if t.Error != nil { + return + } + + t.Total = int(resp.ContentLength) // TODO: int(int64) + if t.Total <= 0 { + t.Error = fmt.Errorf("unknown content length: %d", t.Total) + } + + t.r = resp.Body +} + +func (t *HTTPTransfer) transfer(sniff sniffer) { + // initiate the transfer and monitor it + _, t.Error = io.Copy(io.MultiWriter(t.w, sniff), t.r) + + // if the err is from sniffer ignore it + if _, ok := t.Error.(sniffer); ok { + t.Error = nil + } + + // the next signal will say: "no new bytes received" and its done + t.Current = 0 + t.Done = true +} + +// signal signals the listeners about the last transfer progress. +// it returns false when the done signal is received or there was an error +// in the transfer. +func (t *HTTPTransfer) signal() bool { + select { + case t.updates <- t.Progress: + case <-t.done: + // shutting down signal received + return false + } + + // check the error only after sending the last progress + // if this check was above, the last update won't be sent + if t.Error != nil { + return false + } + + // go on your duties + return true +} diff --git a/28-concurrency/xxx-concurrent-downloader/fetch/httptransfer_fake.go b/28-concurrency/xxx-concurrent-downloader/fetch/httptransfer_fake.go new file mode 100644 index 0000000..488b94e --- /dev/null +++ b/28-concurrency/xxx-concurrent-downloader/fetch/httptransfer_fake.go @@ -0,0 +1,71 @@ +package fetch + +import ( + "errors" + "fmt" + "math/rand" + "time" +) + +func (t *HTTPTransfer) fakeStart() { + jitter() + t.fakeRequest() + t.signal() + + for t.Downloaded < 100 { + jitter() + t.fakeFetch() + + if !t.signal() { + // done signal received or there was an error + break + } + } + + t.fakeFinish() + t.signal() +} + +// request requests to the url and adjusts the total length. +func (t *HTTPTransfer) fakeRequest() { + t.Total = 100 + + if debug { + fmt.Printf("[TRANSFER] started: %s\n", t.URL) + } +} + +// fetch fetches a bit from the resource +func (t *HTTPTransfer) fakeFetch() { + // TODO: right now hanged goroutine may hang the download completion + // needs timeout + // if t.URL == "url1" { + // select {} + // } + + // NOTE: burada sayacli io.Writer kullan + if t.URL == "url1" && t.Downloaded > rand.Intn(50) { + t.Error = errors.New("cekemedim netten") + } + + n := rand.Intn(20) + 1 + if nn := t.Downloaded + n; nn > 100 { + n = 100 - t.Downloaded + } + t.Current = n + t.Downloaded += n +} + +// finish signals the finish signal to the listeners +func (t *HTTPTransfer) fakeFinish() { + t.Current = 0 + t.Done = true + + if debug { + fmt.Printf("[TRANSFER] DONE: %s\n", t.URL) + } +} + +func jitter() { + time.Sleep(time.Millisecond * time.Duration(rand.Intn(500)+1)) +} diff --git a/28-concurrency/xxx-concurrent-downloader/fetch/progress.go b/28-concurrency/xxx-concurrent-downloader/fetch/progress.go new file mode 100644 index 0000000..a85eb34 --- /dev/null +++ b/28-concurrency/xxx-concurrent-downloader/fetch/progress.go @@ -0,0 +1,10 @@ +package fetch + +// Progress contains data about the downloading progress +type Progress struct { + URL string + + Total, Downloaded, Current int + Done bool + Error error +} diff --git a/28-concurrency/xxx-concurrent-downloader/fetch/session.go b/28-concurrency/xxx-concurrent-downloader/fetch/session.go new file mode 100644 index 0000000..ee1f18f --- /dev/null +++ b/28-concurrency/xxx-concurrent-downloader/fetch/session.go @@ -0,0 +1,57 @@ +package fetch + +import ( + "fmt" + "sync" +) + +const debug = true + +// Session manages the downloading process +type Session struct { + done chan bool +} + +// Transfer sends `updates` and terminates when `done` closes +type Transfer interface { + Start(updates chan<- Progress, done <-chan bool) +} + +// NewSession creates a new downloading session +func NewSession() *Session { + return &Session{done: make(chan bool)} +} + +// Start starts the downloading process +func (s *Session) Start(transfers ...Transfer) <-chan Progress { + // a buffered chan may unblock transfers in case of a slow ui + updates := make(chan Progress) + + var wg sync.WaitGroup + wg.Add(len(transfers)) + + for _, t := range transfers { + go func(t Transfer) { + t.Start(updates, s.done) + wg.Done() + }(t) + } + + go func() { + wg.Wait() // wait until all downloads complete + close(updates) // let the watchers (ui) know that we're shutting down + }() + + return updates +} + +// Shutdown stops the downloading process and sends a signal to all parties +func (s *Session) Shutdown() { + // let the transfers know we're shutting down + // when this is done s.updates will be closed in the Start() routine above + close(s.done) + + if debug { + fmt.Printf("[SESSION ] DONE\n") + } +} diff --git a/28-concurrency/xxx-concurrent-downloader/fetch/sniffer.go b/28-concurrency/xxx-concurrent-downloader/fetch/sniffer.go new file mode 100644 index 0000000..7b87429 --- /dev/null +++ b/28-concurrency/xxx-concurrent-downloader/fetch/sniffer.go @@ -0,0 +1,27 @@ +package fetch + +// sniffer converts a function to a sniffer that can sniff from the +// on-going io.Writer call such as request -> file. +// +// This is here just for simplifying the logic of HTTPTransfer. +type sniffer func(p []byte) bool + +// Write satistifes io.Writer interface to sniff from it. +// It can be used through a io.MultiWriter. +func (f sniffer) Write(p []byte) (n int, err error) { + n = len(p) + + // if the sniffer returns false, terminate with a non-nil error. + // it used to abrupt the sniffing process, such as abrupting the + // io.MultiWriter. + if !f(p) { + err = f + } + return +} + +// Error satisfies the Error interface. So the returned error from the +// sniffer.Write func can return itself as an error. This is only used when +// the sniff.Write wants to terminate. So that we can distinguish it from a +// real error. +func (f sniffer) Error() string { return "" } diff --git a/28-concurrency/xxx-concurrent-downloader/fetch/storage.go b/28-concurrency/xxx-concurrent-downloader/fetch/storage.go new file mode 100644 index 0000000..b26cf3d --- /dev/null +++ b/28-concurrency/xxx-concurrent-downloader/fetch/storage.go @@ -0,0 +1,44 @@ +package fetch + +import ( + "fmt" + "io" +) + +// TODO: let main func hand the "TransferFactory" to "Session" +// instead of "StorageFactory" +// +// Because: "Session" duplicates almost everything for "Transfer" + +// StorageFactory func allows to switch storage implementations +// When called it may return a FileStore +// The transfer will call its Write method +// +// Why a func rather than an interface? +// Because: We don't have to store any state +// Storage will return its own io.Writer and the state will be in it +// However, before invoking the storage, we don't need any state +// We let us the storage manage its own state, we don't care about it +type StorageFactory func(url string) io.Writer + +// FileStorage is the default storage mechanism for the downloader +// It writes to files +type FileStorage struct { + url string + saved int +} + +// FileStorageFactory creates and returns a new FileStorage +func FileStorageFactory(url string) io.Writer { + return &FileStorage{url: url} +} + +func (f *FileStorage) Write(p []byte) (int, error) { + if debug { + fmt.Println("[FILESTORAGE]", string(p), "for", f.url) + } + // TODO: + // if not exists create it + // if exists update it + return 0, nil +} diff --git a/28-concurrency/xxx-concurrent-downloader/fetch/ui.go b/28-concurrency/xxx-concurrent-downloader/fetch/ui.go new file mode 100644 index 0000000..cd38672 --- /dev/null +++ b/28-concurrency/xxx-concurrent-downloader/fetch/ui.go @@ -0,0 +1,96 @@ +package fetch + +import ( + "fmt" + "time" + + "github.com/inancgumus/screen" +) + +const refreshPeriod = time.Second / 10 + +// uiProgress is the default UI for the downloader +type uiProgress struct { + urls []string + transfers map[string]Progress +} + +// UI listens for the progress updates from the updates chan +// and it refreshes the ui +func UI(updates <-chan Progress) { + ui := &uiProgress{transfers: make(map[string]Progress)} + + // NOTE: we didn't use time.After here directly + // because doing so can create a lot of Timer chans unnecessarily + // instead we're just waiting on the same timer value + tick := time.After(refreshPeriod) + for { + select { + case p, ok := <-updates: + // if no more updates close the ui + if !ok { + ui.shutdown() + return + } + ui.update(p) + + case <-tick: + // `case <-tick:` allows updating the ui independently + // from the progress update signals. or the ui would hang + // the updaters (transfers). + ui.refresh() + tick = time.After(refreshPeriod) + } + } + +} + +// shutdown refreshes the ui for the last time and closes it +func (ui *uiProgress) shutdown() { + ui.refresh() + + if debug { + fmt.Printf("[ UI ] DONE\n") + } +} + +// update updates the progress data from the received message +func (ui *uiProgress) update(p Progress) { + if _, ok := ui.transfers[p.URL]; !ok { + ui.urls = append(ui.urls, p.URL) + } + + // update the latest progress for the url + ui.transfers[p.URL] = p +} + +// refresh refreshes the UI with the latest progress +func (ui *uiProgress) refresh() { + if !debug { + screen.Clear() + screen.MoveTopLeft() + } + + var total, downloaded int + + for _, u := range ui.urls { + p := ui.transfers[u] + + msg := "Downloading" + if p.Done && p.Error == nil { + msg = "👍 Completed" + } + if p.Error != nil { + msg = fmt.Sprintf("❌ %s", p.Error) + } + + fmt.Println(p.URL) + fmt.Printf("\t%d/%d\n", p.Downloaded, p.Total) + fmt.Printf("\t%s\n", msg) + + total += p.Total + downloaded += p.Downloaded + } + + fmt.Printf("\n%s %d/%d\n", "TOTAL DOWNLOADED BYTES:", downloaded, total) +} diff --git a/28-concurrency/xxx-concurrent-downloader/main.go b/28-concurrency/xxx-concurrent-downloader/main.go new file mode 100644 index 0000000..ea0fed0 --- /dev/null +++ b/28-concurrency/xxx-concurrent-downloader/main.go @@ -0,0 +1,103 @@ +package main + +import ( + "time" + + dl "github.com/inancgumus/course-experimentations/xxx-concurrent-downloader/0419-experimental/fetch" +) + +// +// +--- transfer #1 (on done/error signals and quits) +// | +// UI <--+--- transfer #2 +// | +// +--- transfer #N +// ^ +// | +// SESSION ------+ launches goroutines +// + +// +// +--- transfer #1 +// | +// UI <--+--- transfer #2 +// | +// +--- transfer #N +// ^ +// | +// SESSION ------+ launches goroutines +// + +// +// +--- transfer #1 +// | +// UI <--+--- transfer #2 +// ^ | +// | +--- transfer #N +// | ^ +// | | +// +--------- SESSION +// +// Session can close the transfers with session.Shutdown() +// (through session.done chan) +// +// Session closes the watcher chan when the transfers end +// Possible problem: If any of the transfers never quit, session will hang. +// I think, I'm going to manage that using context.Context in the transfers. +// + +func main() { + // ======================================================================== + + // to := time.Second * 5 + // res, err := dl.HTTPGet("https://jsonplaceholder.typicode.com/todos/1", to) + // if err != nil { + // fmt.Println(err) + // return + // } + // defer res.Body.Close() + + // io.Copy(os.Stdout, res.Body) + + // return + + // ======================================================================== + + sess := dl.NewSession() + + // simulate a manual shutdown + // time.AfterFunc(time.Second, func() { + // sess.Shutdown() + // }) + + to := time.Second * 5 + transfers := []dl.Transfer{ + dl.NewHTTPTransfer("https://inancgumus.github.io/samples/jpg-beach-1020x670-160kb.jpg", to), + dl.NewHTTPTransfer("https://inancgumus.github.io/samples/jpg-beach-1020x670-610kb.jpg", to), + dl.NewHTTPTransfer("https://inancgumus.github.io/samples/jpg-beach-1020x670-80kb.jpg", to), + dl.NewHTTPTransfer("https://inancgumus.github.io/samples/jpg-beach-1900x1250-1700kb.jpg", to), + } + + // transfers := []dl.Transfer{ + // dl.NewHTTPTransfer(ctx, "http://inanc.io/1"), + // dl.NewHTTPTransfer(ctx, "http://inanc.io/2"), + // dl.NewHTTPTransfer(ctx, "http://inanc.io/3"), + // } + + dl.UI(sess.Start(transfers...)) + + // results := dl.Drain(sess.Start(urls)) + // for _, r := range results { + // fmt.Printf("%s [err: %v — %d/%d]\n", + // r.URL, r.Error, r.Downloaded, r.Total) + // } + + // how to handle ctrl+c signals? + // register a signal + // let the downloader now that the operation halts + // with Downloader.onCancel or "context.Context" + + // run with: GOTRACEBACK=all go run -race main.go + // time.Sleep(time.Second * 2) + // panic("give me the stack trace") +} diff --git a/x-tba/xxx-log-parser-json/log.txt b/x-tba/log-parser-json/log.txt similarity index 100% rename from x-tba/xxx-log-parser-json/log.txt rename to x-tba/log-parser-json/log.txt diff --git a/x-tba/xxx-log-parser-json/log_err_missing.txt b/x-tba/log-parser-json/log_err_missing.txt similarity index 100% rename from x-tba/xxx-log-parser-json/log_err_missing.txt rename to x-tba/log-parser-json/log_err_missing.txt diff --git a/x-tba/xxx-log-parser-json/log_err_negative.txt b/x-tba/log-parser-json/log_err_negative.txt similarity index 100% rename from x-tba/xxx-log-parser-json/log_err_negative.txt rename to x-tba/log-parser-json/log_err_negative.txt diff --git a/x-tba/xxx-log-parser-json/log_err_str.txt b/x-tba/log-parser-json/log_err_str.txt similarity index 100% rename from x-tba/xxx-log-parser-json/log_err_str.txt rename to x-tba/log-parser-json/log_err_str.txt diff --git a/x-tba/xxx-log-parser-json/main.go b/x-tba/log-parser-json/main.go similarity index 100% rename from x-tba/xxx-log-parser-json/main.go rename to x-tba/log-parser-json/main.go diff --git a/x-tba/xxx-project-pricings/main.go b/x-tba/project-pricings/main.go similarity index 100% rename from x-tba/xxx-project-pricings/main.go rename to x-tba/project-pricings/main.go diff --git a/x-tba/xxx-project-pricings/parse.go b/x-tba/project-pricings/parse.go similarity index 100% rename from x-tba/xxx-project-pricings/parse.go rename to x-tba/project-pricings/parse.go diff --git a/x-tba/xxx-project-pricings/parse_test.go b/x-tba/project-pricings/parse_test.go similarity index 100% rename from x-tba/xxx-project-pricings/parse_test.go rename to x-tba/project-pricings/parse_test.go diff --git a/x-tba/xxx-project-pricings/print.go b/x-tba/project-pricings/print.go similarity index 100% rename from x-tba/xxx-project-pricings/print.go rename to x-tba/project-pricings/print.go diff --git a/x-tba/xxx-project-pricings/property.go b/x-tba/project-pricings/property.go similarity index 100% rename from x-tba/xxx-project-pricings/property.go rename to x-tba/project-pricings/property.go diff --git a/x-tba/xx-slicing-allocs-gotcha/main.go b/x-tba/slicing-allocs-gotcha/main.go similarity index 100% rename from x-tba/xx-slicing-allocs-gotcha/main.go rename to x-tba/slicing-allocs-gotcha/main.go diff --git a/x-tba/xx-slicing-allocs-gotcha/nums/main.go b/x-tba/slicing-allocs-gotcha/nums/main.go similarity index 100% rename from x-tba/xx-slicing-allocs-gotcha/nums/main.go rename to x-tba/slicing-allocs-gotcha/nums/main.go diff --git a/x-tba/swapi-api-client/film.go b/x-tba/swapi-api-client/film.go new file mode 100644 index 0000000..cc639fa --- /dev/null +++ b/x-tba/swapi-api-client/film.go @@ -0,0 +1,38 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "strings" +) + +// Film represents a single Star Wars film +type Film struct { + // All fields should be exported to be decoded + Title string `json:"title"` + Opening string `json:"opening_crawl"` + Starships []string `json:"starships"` +} + +func (f Film) String() string { + var buf strings.Builder + + fmt.Fprintln(&buf, f.Title) + fmt.Fprintln(&buf, strings.Repeat("-", len(f.Title))) + buf.WriteByte('\n') + fmt.Fprintln(&buf, f.Opening) + + return buf.String() +} + +// fetchFilm returns a Film resource by its id +func fetchFilm(ctx context.Context, id int) (film Film, err error) { + fmt.Println("requesting", fmt.Sprintf(swapi+"films/%d/", id)) + c, err := request(ctx, fmt.Sprintf(swapi+"films/%d/", id)) + if err != nil { + return film, err + } + + return film, json.Unmarshal(c, &film) +} diff --git a/x-tba/swapi-api-client/main.go b/x-tba/swapi-api-client/main.go new file mode 100644 index 0000000..aab7a26 --- /dev/null +++ b/x-tba/swapi-api-client/main.go @@ -0,0 +1,84 @@ +package main + +import ( + "context" + "fmt" + "os" + "strconv" + "time" +) + +// const +// nil, string, int, float64, bool, comparison +// variables +// multiple short variables +// assignment? +// if +// error handling +// functions +// returns +// defer +// struct +// encoding/json +// pointers +// concurrency +// select +// chan receive +// fmt +// Printf +// Sprintf +// Errorf +// net/http +// Get +// context/Context + +// TODO: convert fmt calls to log +// TODO: you can make the fetcher a library and main package the user +// TODO: you can generate an html for the ship details? template pkg. + +const timeout = 10 * time.Second + +func main() { + args := os.Args[1:] + quit("give me a film id", len(args) != 1) + + id, err := strconv.Atoi(args[0]) + quit("film id is incorrrect", err) + + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + + // TODO: print the ship details to a text file (or any io.Writer) + + film, err := fetchFilm(ctx, id) + quit("Error occurred while fetching the film data", err) + fmt.Println(film) + + // a channel also can be used to print as they come + ships := make([]Starship, len(film.Starships)) + err = fetchStarships(ctx, film.Starships, ships) + quit("Error occurred while fetching starships", err) + + fmt.Println("Ships used in the movie:") + fmt.Println("------------------------") + for _, ship := range ships { + fmt.Println(ship) + } +} + +func quit(message string, cond interface{}) { + var quit bool + + switch v := cond.(type) { + case error: + quit = true + message += ": " + v.Error() + case bool: + quit = v + } + + if quit { + fmt.Fprintln(os.Stderr, message) + os.Exit(1) + } +} diff --git a/x-tba/swapi-api-client/request.go b/x-tba/swapi-api-client/request.go new file mode 100644 index 0000000..8aed440 --- /dev/null +++ b/x-tba/swapi-api-client/request.go @@ -0,0 +1,44 @@ +package main + +import ( + "context" + "fmt" + "io" + "io/ioutil" + "net/http" +) + +// MaxResponseSize limits the response bytes from the API +const MaxResponseSize = 2 << 16 + +// creating a robust http getter (lecture? :)) +func request(ctx context.Context, url string) ([]byte, error) { + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return nil, err + } + + req = req.WithContext(ctx) + + resp, err := http.DefaultClient.Do(req) + if err != nil { + // Get the error from the context. + // It may contain more useful data. + select { + case <-ctx.Done(): + err = ctx.Err() + default: + } + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("Bad Status: %d", resp.StatusCode) + } + + // Prevents the api to shoot us unlimited amount of data + r := io.LimitReader(resp.Body, MaxResponseSize) + + return ioutil.ReadAll(r) +} diff --git a/x-tba/swapi-api-client/starship.go b/x-tba/swapi-api-client/starship.go new file mode 100644 index 0000000..f98cc68 --- /dev/null +++ b/x-tba/swapi-api-client/starship.go @@ -0,0 +1,69 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "regexp" + "strconv" + "strings" +) + +// Starship represents a Star Wars space ship +type Starship struct { + // All fields should be exported to be decoded + Name string `json:"name"` + Credits json.Number `json:"cost_in_credits"` + Rating float64 `json:"hyperdrive_rating,string"` +} + +func (ship Starship) String() string { + var buf strings.Builder + + fmt.Fprintf(&buf, "Ship Name: %s\n", ship.Name) + fmt.Fprintf(&buf, "Price : %s\n", ship.Credits) + + return buf.String() +} + +// TODO: return error instead of quit() +func fetchStarships(ctx context.Context, shipUrls []string, ships []Starship) error { + ships = ships[:0] + + re := regexp.MustCompile(`/([0-9]+)/$`) + for _, url := range shipUrls { + ids := re.FindAllStringSubmatch(url, -1) + if ids == nil { + continue + } + + sid := ids[0][1] + id, _ := strconv.Atoi(sid) + + // TODO: goroutine + ship, err := fetchStarship(ctx, id) + if err != nil { + return err + } + + ships = append(ships, ship) + } + return nil +} + +// fetchStarship returns Starship info by its id +func fetchStarship(ctx context.Context, id int) (ship Starship, err error) { + c, err := request(ctx, fmt.Sprintf(swapi+"starships/%d/", id)) + if err != nil { + return ship, err + } + + // -> If your response body is small enough, + // just read it all into memory using ioutil.ReadAll + // and use json.Unmarshal. + // + // -> Do not use json.Decoder if you are not dealing + // with JSON streaming. + + return ship, json.Unmarshal(c, &ship) +} diff --git a/x-tba/swapi-api-client/swapi.go b/x-tba/swapi-api-client/swapi.go new file mode 100644 index 0000000..e78c23a --- /dev/null +++ b/x-tba/swapi-api-client/swapi.go @@ -0,0 +1,3 @@ +package main + +const swapi = "https://swapi.co/api/" diff --git a/x-tba/tictactoe/01-without-funcs/main.go b/x-tba/tictactoe/01-without-funcs/main.go new file mode 100644 index 0000000..fbf9acc --- /dev/null +++ b/x-tba/tictactoe/01-without-funcs/main.go @@ -0,0 +1,141 @@ +package main + +import ( + "bufio" + "fmt" + "os" + "strconv" + "strings" +) + +const ( + // skin options :-) + emptyMark = "☆" + mark1 = "💀" + mark2 = "🎈" + banner = ` +~~~~~~~~~~~~~~~ + TIC~TAC~TOE +~~~~~~~~~~~~~~~` + + maxTurn = 9 + + prompt = "\n%s [1-9]: " +) + +func main() { + // ------------------------------------------------- + // INITIALIZE THE GAME + // ------------------------------------------------- + + fmt.Println(banner) + + in := bufio.NewScanner(os.Stdin) + + var ( + turn int + player = mark1 + board = [3][3]string{ + {emptyMark, emptyMark, emptyMark}, + {emptyMark, emptyMark, emptyMark}, + {emptyMark, emptyMark, emptyMark}, + } + ) + +game: + for { + // ------------------------------------------------- + // PRINT THE BOARD AND THE PROMPT + // ------------------------------------------------- + fmt.Println() + fmt.Println("---+----+---") + for _, line := range board { + fmt.Printf("%s\n", strings.Join(line[:], " | ")) + fmt.Println("---+----+---") + } + fmt.Printf(prompt, player) + + // get the input + if !in.Scan() { + break + } + + // ------------------------------------------------- + // CHECK THE MOVE AND PLAY + // ------------------------------------------------- + var ( + pos int + row, col int + ) + + // Atoi already return 0 on error; no need to check + // it for the following switch to work + pos, _ = strconv.Atoi(in.Text()) + + switch { + case pos >= 7 && pos <= 9: + row = 2 + case pos >= 4 && pos <= 6: + row = 1 + case pos >= 1 && pos <= 3: + row = 0 + default: + fmt.Println("\n>>>", "wrong position!") + continue + } + + col = pos - row*3 - 1 + + if board[row][col] != emptyMark { + fmt.Println("\n>>>", "already played!") + continue + } + + // put a mark on the board + board[row][col] = player + + // ------------------------------------------------- + // IS THERE A WINNER? OR IS IT A TIE? + // ------------------------------------------------- + var won bool + + for _, m := range [2]string{mark1, mark2} { + b, mmm := board, strings.Repeat(m, 3) + + won = /* horizontals */ + strings.Join(b[0][:], "") == mmm || + strings.Join(b[1][:], "") == mmm || + strings.Join(b[2][:], "") == mmm || + + /* verticals */ + b[0][0]+b[1][0]+b[2][0] == mmm || + b[0][1]+b[1][1]+b[2][1] == mmm || + b[0][2]+b[1][2]+b[2][2] == mmm || + + /* diagonals */ + b[0][0]+b[1][1]+b[2][2] == mmm || + b[0][2]+b[1][1]+b[2][0] == mmm + + if won { + break + } + } + + if won { + fmt.Printf("\nWINNER: %s\n", player) + break game + } else if turn++; turn == maxTurn { + fmt.Printf("\nTIE!\n") + break game + } + + // ------------------------------------------------- + // CHANGE THE MARKER TO THE NEXT PLAYER + // ------------------------------------------------- + if player == mark1 { + player = mark2 + } else { + player = mark1 + } + } +} diff --git a/x-tba/tictactoe/02-with-funcs/main.go b/x-tba/tictactoe/02-with-funcs/main.go new file mode 100644 index 0000000..ddd1b31 --- /dev/null +++ b/x-tba/tictactoe/02-with-funcs/main.go @@ -0,0 +1,156 @@ +package main + +import ( + "bufio" + "fmt" + "os" + "strconv" + "strings" +) + +const ( + // skin options :-) + emptyMark = "☆" + mark1 = "💀" + mark2 = "🎈" + banner = ` +~~~~~~~~~~~~~~~ + TIC~TAC~TOE +~~~~~~~~~~~~~~~` + + maxTurn = 9 +) + +func main() { + fmt.Println(banner) + + loop(bufio.NewScanner(os.Stdin)) +} + +func loop(in *bufio.Scanner) { + var ( + turn int + player = mark1 + board = createBoard() + ) + + for { + prompt(board, player) + + if !in.Scan() { + break + } + + msg := play(board, player, getMove(in.Text())) + if msg != "" { + fmt.Println("\n>>>", msg) + continue + } + + turn++ + if msg := finito(board, turn, player); msg != "" { + printBoard(board, player) + fmt.Printf("\n%s\n", msg) + break + } + + player = switchTo(player) + } +} + +func createBoard() [][]string { + return [][]string{ + {emptyMark, emptyMark, emptyMark}, + {emptyMark, emptyMark, emptyMark}, + {emptyMark, emptyMark, emptyMark}, + } +} + +func prompt(board [][]string, player string) { + printBoard(board, player) + fmt.Printf("\n%s [1-9]: ", player) +} + +func printBoard(board [][]string, player string) { + fmt.Println() + fmt.Println("---+----+---") + for _, line := range board { + fmt.Printf("%s\n", strings.Join(line, " | ")) + fmt.Println("---+----+---") + } +} + +func getMove(move string) int { + // Atoi already return 0 on error; no need to check + // it for the following switch to work + pos, _ := strconv.Atoi(move) + return pos +} + +func play(board [][]string, player string, pos int) string { + var row int + + switch { + case pos >= 7 && pos <= 9: + row = 2 + case pos >= 4 && pos <= 6: + row = 1 + case pos >= 1 && pos <= 3: + row = 0 + default: + return "wrong position" + } + + col := pos - row*3 - 1 + + if board[row][col] != emptyMark { + return "already played!" + } + + // put the player + board[row][col] = player + + return "" +} + +func finito(board [][]string, turn int, player string) string { + switch { + case won(board): + return fmt.Sprintf("WINNER: %s", player) + case turn == maxTurn: + return "TIE!" + } + return "" +} + +func won(board [][]string) (won bool) { + for _, m := range [2]string{mark1, mark2} { + b, mmm := board, strings.Repeat(m, 3) + + won = /* horizontals */ + strings.Join(b[0], "") == mmm || + strings.Join(b[1], "") == mmm || + strings.Join(b[2], "") == mmm || + + /* verticals */ + b[0][0]+b[1][0]+b[2][0] == mmm || + b[0][1]+b[1][1]+b[2][1] == mmm || + b[0][2]+b[1][2]+b[2][2] == mmm || + + /* diagonals */ + b[0][0]+b[1][1]+b[2][2] == mmm || + b[0][2]+b[1][1]+b[2][0] == mmm + + if won { + return true + } + } + return false +} + +func switchTo(player string) string { + if player == mark1 { + return mark2 + } + return mark1 +} diff --git a/x-tba/tictactoe/03-with-structs/main.go b/x-tba/tictactoe/03-with-structs/main.go new file mode 100644 index 0000000..f907085 --- /dev/null +++ b/x-tba/tictactoe/03-with-structs/main.go @@ -0,0 +1,162 @@ +package main + +import ( + "bufio" + "fmt" + "os" + "strconv" + "strings" +) + +const ( + // skin options :-) + emptyMark = "☆" + mark1 = "💀" + mark2 = "🎈" + banner = ` +~~~~~~~~~~~~~~~ + TIC~TAC~TOE +~~~~~~~~~~~~~~~` + + maxTurn = 9 +) + +type game struct { + board [][]string + turn int + player string +} + +func main() { + fmt.Println(banner) + + loop(bufio.NewScanner(os.Stdin)) +} + +func loop(in *bufio.Scanner) { + g := game{ + player: mark1, + board: createBoard(), + } + + for { + prompt(g) + + if !in.Scan() { + break + } + + msg := play(g, getMove(in.Text())) + if msg != "" { + fmt.Println("\n>>>", msg) + continue + } + + g.turn++ + if msg := finito(g); msg != "" { + printBoard(g.board) + fmt.Printf("\n%s\n", msg) + break + } + + g.player = switchTo(g.player) + } +} + +func createBoard() [][]string { + return [][]string{ + {emptyMark, emptyMark, emptyMark}, + {emptyMark, emptyMark, emptyMark}, + {emptyMark, emptyMark, emptyMark}, + } +} + +func prompt(g game) { + printBoard(g.board) + fmt.Printf("\n%s [1-9]: ", g.player) +} + +func printBoard(board [][]string) { + fmt.Println() + fmt.Println("---+----+---") + for _, line := range board { + fmt.Printf("%s\n", strings.Join(line, " | ")) + fmt.Println("---+----+---") + } +} + +func getMove(move string) int { + // Atoi already return 0 on error; no need to check + // it for the following switch to work + pos, _ := strconv.Atoi(move) + return pos +} + +// NOTE: manipulates the game object +func play(g game, pos int) string { + var row int + + switch { + case pos >= 7 && pos <= 9: + row = 2 + case pos >= 4 && pos <= 6: + row = 1 + case pos >= 1 && pos <= 3: + row = 0 + default: + return "wrong position" + } + + col := pos - row*3 - 1 + + if g.board[row][col] != emptyMark { + return "already played!" + } + + // put the player + g.board[row][col] = g.player + + return "" +} + +func finito(g game) string { + switch { + case won(g.board): + return fmt.Sprintf("WINNER: %s", g.player) + case g.turn == maxTurn: + return "TIE!" + } + return "" +} + +func won(board [][]string) (won bool) { + for _, m := range [2]string{mark1, mark2} { + b, mmm := board, strings.Repeat(m, 3) + + won = /* horizontals */ + strings.Join(b[0], "") == mmm || + strings.Join(b[1], "") == mmm || + strings.Join(b[2], "") == mmm || + + /* verticals */ + b[0][0]+b[1][0]+b[2][0] == mmm || + b[0][1]+b[1][1]+b[2][1] == mmm || + b[0][2]+b[1][2]+b[2][2] == mmm || + + /* diagonals */ + b[0][0]+b[1][1]+b[2][2] == mmm || + b[0][2]+b[1][1]+b[2][0] == mmm + + if won { + return true + } + } + return false +} + +func switchTo(player string) string { + if player == mark1 { + return mark2 + } + return mark1 +} diff --git a/x-tba/tictactoe/04-with-methods/main.go b/x-tba/tictactoe/04-with-methods/main.go new file mode 100644 index 0000000..cbbff3b --- /dev/null +++ b/x-tba/tictactoe/04-with-methods/main.go @@ -0,0 +1,158 @@ +package main + +import ( + "bufio" + "fmt" + "os" + "strconv" + "strings" +) + +const ( + // skin options :-) + emptyMark = "☆" + mark1 = "💀" + mark2 = "🎈" + banner = ` +~~~~~~~~~~~~~~~ + TIC~TAC~TOE +~~~~~~~~~~~~~~~` + + maxTurn = 9 +) + +func main() { + fmt.Println(banner) + + loop(bufio.NewScanner(os.Stdin)) +} + +type game struct { + board [][]string + turn int + player string +} + +func loop(in *bufio.Scanner) { + g := newGame() + + for { + g.prompt() + + if !in.Scan() { + break + } + + // Atoi already return 0 on error; no need to check + // it for the following switch to work + pos, _ := strconv.Atoi(in.Text()) + + msg := g.play(pos) + if msg != "" { + fmt.Println("\n>>>", msg) + continue + } + + g.turn++ + if msg := g.finito(); msg != "" { + g.print() + fmt.Printf("\n%s\n", msg) + break + } + + g.player = switchTo(g.player) + } +} + +func newGame() game { + return game{ + player: mark1, + board: [][]string{ + {emptyMark, emptyMark, emptyMark}, + {emptyMark, emptyMark, emptyMark}, + {emptyMark, emptyMark, emptyMark}, + }, + } +} + +func (g game) prompt() { + g.print() + fmt.Printf("\n%s [1-9]: ", g.player) +} + +func (g game) print() { + fmt.Println() + fmt.Println("---+----+---") + for _, line := range g.board { + fmt.Printf("%s\n", strings.Join(line, " | ")) + fmt.Println("---+----+---") + } +} + +func (g game) play(pos int) string { + var row int + + switch { + case pos >= 7 && pos <= 9: + row = 2 + case pos >= 4 && pos <= 6: + row = 1 + case pos >= 1 && pos <= 3: + row = 0 + default: + return "wrong position" + } + + col := pos - row*3 - 1 + + if g.board[row][col] != emptyMark { + return "already played!" + } + + // put the player + g.board[row][col] = g.player + + return "" +} + +func (g game) finito() string { + switch { + case g.won(): + return fmt.Sprintf("WINNER: %s", g.player) + case g.turn == maxTurn: + return "TIE!" + } + return "" +} + +func (g game) won() (won bool) { + for _, m := range [2]string{mark1, mark2} { + b, mmm := g.board, strings.Repeat(m, 3) + + won = /* horizontals */ + strings.Join(b[0], "") == mmm || + strings.Join(b[1], "") == mmm || + strings.Join(b[2], "") == mmm || + + /* verticals */ + b[0][0]+b[1][0]+b[2][0] == mmm || + b[0][1]+b[1][1]+b[2][1] == mmm || + b[0][2]+b[1][2]+b[2][2] == mmm || + + /* diagonals */ + b[0][0]+b[1][1]+b[2][2] == mmm || + b[0][2]+b[1][1]+b[2][0] == mmm + + if won { + return true + } + } + return false +} + +func switchTo(player string) string { + if player == mark1 { + return mark2 + } + return mark1 +} diff --git a/x-tba/tictactoe/05-with-pointers/main.go b/x-tba/tictactoe/05-with-pointers/main.go new file mode 100644 index 0000000..9d2fa75 --- /dev/null +++ b/x-tba/tictactoe/05-with-pointers/main.go @@ -0,0 +1,166 @@ +package main + +import ( + "bufio" + "fmt" + "os" + "strconv" + "strings" +) + +func main() { + fmt.Println(` +~~~~~~~~~~~~~~~ + TIC~TAC~TOE +~~~~~~~~~~~~~~~`) + + loop(bufio.NewScanner(os.Stdin)) +} + +const ( + // skin options :-) + emptyMark = "☆" + mark1 = "💀" + mark2 = "🎈" + + maxTurn = 9 +) + +type game struct { + board [][]string + turn int + player string + + buf strings.Builder +} + +func loop(in *bufio.Scanner) { + g := newGame() + + for { + g.prompt() + + if !in.Scan() { + break + } + + // Atoi already return 0 on error; no need to check + // it for the following switch to work + pos, _ := strconv.Atoi(in.Text()) + + msg := g.play(pos) + if msg != "" { + fmt.Println("\n>>>", msg) + continue + } + + if msg := g.finito(); msg != "" { + fmt.Printf("\n%s%s\n", g, msg) + break + } + + g.next() + } +} + +func newGame() *game { + return &game{ + player: mark1, + board: [][]string{ + {emptyMark, emptyMark, emptyMark}, + {emptyMark, emptyMark, emptyMark}, + {emptyMark, emptyMark, emptyMark}, + }, + } +} + +func (g *game) prompt() { + fmt.Printf("\n%s%s [1-9]: ", g, g.player) +} + +func (g *game) String() string { + g.buf.Reset() + + g.buf.WriteRune('\n') + g.buf.WriteString("---+----+---\n") + + for _, line := range g.board { + g.buf.WriteString(strings.Join(line, " | ")) + g.buf.WriteRune('\n') + + g.buf.WriteString("---+----+---\n") + } + + return g.buf.String() +} + +func (g *game) play(pos int) string { + var row int + + switch { + case pos >= 7 && pos <= 9: + row = 2 + case pos >= 4 && pos <= 6: + row = 1 + case pos >= 1 && pos <= 3: + row = 0 + default: + return "wrong position" + } + + col := pos - row*3 - 1 + + if g.board[row][col] != emptyMark { + return "already played!" + } + + // put the player + g.board[row][col] = g.player + + return "" +} + +func (g *game) finito() string { + g.turn++ + + switch { + case g.won(): + return fmt.Sprintf("WINNER: %s", g.player) + case g.turn == maxTurn: + return "TIE!" + } + return "" +} + +func (g *game) won() (won bool) { + for _, m := range [2]string{mark1, mark2} { + b, mmm := g.board, strings.Repeat(m, 3) + + won = /* horizontals */ + strings.Join(b[0], "") == mmm || + strings.Join(b[1], "") == mmm || + strings.Join(b[2], "") == mmm || + + /* verticals */ + b[0][0]+b[1][0]+b[2][0] == mmm || + b[0][1]+b[1][1]+b[2][1] == mmm || + b[0][2]+b[1][2]+b[2][2] == mmm || + + /* diagonals */ + b[0][0]+b[1][1]+b[2][2] == mmm || + b[0][2]+b[1][1]+b[2][0] == mmm + + if won { + return true + } + } + return false +} + +func (g *game) next() { + if g.player == mark1 { + g.player = mark2 + } else { + g.player = mark1 + } +} diff --git a/x-tba/tictactoe/06-refactor/game.go b/x-tba/tictactoe/06-refactor/game.go new file mode 100644 index 0000000..7b4658e --- /dev/null +++ b/x-tba/tictactoe/06-refactor/game.go @@ -0,0 +1,152 @@ +package main + +import ( + "strings" +) + +type state int + +const ( + maxTurn = 9 + + wrongPosition = -2 + + statePlaying state = iota + stateWon + stateTie + + stateAlreadyPlayed + stateWrongPosition +) + +type game struct { + board [][]string + + turn int + player string + + skin // embed the skin + logger // embed the logger +} + +func newGame(s skin, l logger) *game { + return &game{ + player: s.mark1, + board: [][]string{ + {s.empty, s.empty, s.empty}, + {s.empty, s.empty, s.empty}, + {s.empty, s.empty, s.empty}, + }, + skin: s, + logger: l, + } +} + +func (g *game) play(pos int) state { + if st := g.move(pos); st != statePlaying { + return st + } + + g.turn++ // increment the turn + + // first check the winner then check the tie + // or the last mover won't win + switch { + case g.won(): + return stateWon + case g.turn == maxTurn: + return stateTie + } + + g.changePlayer() + return statePlaying +} + +func (g *game) move(pos int) state { + row, col := position(pos) + if row+col == wrongPosition { + return stateWrongPosition + } + + if g.board[row][col] != g.empty { + return stateAlreadyPlayed + } + + // put the player's mark on the board + g.board[row][col] = g.player + + return statePlaying +} + +// we can detect the winning state just by comparing the strings +// because, the game board is a bunch of strings +func (g *game) won() (won bool) { + for _, m := range [2]string{g.mark1, g.mark2} { + b, mmm := g.board, strings.Repeat(m, 3) + + won = /* horizontals */ + strings.Join(b[0], "") == mmm || + strings.Join(b[1], "") == mmm || + strings.Join(b[2], "") == mmm || + + /* verticals */ + b[0][0]+b[1][0]+b[2][0] == mmm || + b[0][1]+b[1][1]+b[2][1] == mmm || + b[0][2]+b[1][2]+b[2][2] == mmm || + + /* diagonals */ + b[0][0]+b[1][1]+b[2][2] == mmm || + b[0][2]+b[1][1]+b[2][0] == mmm + + if won { + return true + } + } + return false +} + +// this method should have a pointer receiver +// because, it changes the game value +func (g *game) changePlayer() { + if g.player == g.mark1 { + g.player = g.mark2 + } else { + g.player = g.mark1 + } +} + +func (g *game) print() { + g.Println() + g.Println(g.header) + + for i, line := range g.board { + g.Print(g.separator) + + for _, m := range line { + g.Printf("%2s%s", m, g.separator) + } + + if i+1 != len(g.board) { + g.Printf("\n%s\n", g.middle) + } + } + + g.Printf("\n%s\n", g.footer) +} + +// this function doesn't depend on the game state +// so, make it a function instead of a method +func position(pos int) (row, col int) { + switch { + case pos >= 1 && pos <= 3: + row = 0 + case pos >= 4 && pos <= 6: + row = 1 + case pos >= 7 && pos <= 9: + row = 2 + default: + return -1, -1 + } + + return row, pos - row*3 - 1 +} diff --git a/x-tba/tictactoe/06-refactor/logger.go b/x-tba/tictactoe/06-refactor/logger.go new file mode 100644 index 0000000..696cb7f --- /dev/null +++ b/x-tba/tictactoe/06-refactor/logger.go @@ -0,0 +1,19 @@ +package main + +type logger struct { + print func(...interface{}) (int, error) + printf func(fmt string, args ...interface{}) (int, error) + println func(...interface{}) (int, error) +} + +func (l logger) Print(args ...interface{}) { + l.print(args...) +} + +func (l logger) Printf(fmt string, args ...interface{}) { + l.printf(fmt, args...) +} + +func (l logger) Println(args ...interface{}) { + l.println(args...) +} diff --git a/x-tba/tictactoe/06-refactor/main.go b/x-tba/tictactoe/06-refactor/main.go new file mode 100644 index 0000000..dbaa918 --- /dev/null +++ b/x-tba/tictactoe/06-refactor/main.go @@ -0,0 +1,93 @@ +package main + +import ( + "bufio" + "fmt" + "os" + "strconv" + + c "github.com/fatih/color" + rainbow "github.com/guineveresaenger/golang-rainbow" +) + +// TODO: move the main logic into a helper + +func main() { + const banner = ` +~~~~~~~~~~~~~~~~ + TIC~TAC~TOE +~~~~~~~~~~~~~~~~` + + in := bufio.NewScanner(os.Stdin) + sk := selectSkin(in) + lg := logger{ + print: fmt.Print, + printf: fmt.Printf, + println: fmt.Println, + } + + for { + g := newGame(sk, lg) + + game: + for { + rainbow.Rainbow(banner, 3) + g.print() + fmt.Printf(c.CyanString("\n%s [1-9]: ", g.player)) + + if !in.Scan() { + break + } + + pos, _ := strconv.Atoi(in.Text()) + + switch st := g.play(pos); st { + case stateAlreadyPlayed, stateWrongPosition: + announce(g, st) + continue + case stateWon, stateTie: + announce(g, st) + break game + } + g.Print("\033[2J") + + } + + fmt.Print(c.MagentaString("One more game? [y/n]: ")) + if in.Scan(); in.Text() != "y" { + fmt.Println("OK, bye!") + break + } + } +} + +func announce(g *game, st state) { + red := c.New(c.FgRed, c.Bold) + green := c.New(c.BgBlack, c.FgGreen, c.Bold) + + switch st { + case stateAlreadyPlayed, stateWrongPosition: + red.Printf("\n>>> You can't play there!\n") + case stateWon: + g.print() + green.Printf("\nWINNER: %s\n", g.player) + case stateTie: + g.print() + green.Printf("\nTIE!\n") + } +} + +func selectSkin(in *bufio.Scanner) skin { + fmt.Println(c.MagentaString("Our finest selection of skins:")) + for name := range skins { + fmt.Printf("- %s\n", name) + } + + fmt.Print(c.GreenString("\nEnter the name of the skin: ")) + + in.Scan() + if sk, ok := skins[in.Text()]; ok { + return sk + } + return defaultSkin +} diff --git a/x-tba/tictactoe/06-refactor/resources.md b/x-tba/tictactoe/06-refactor/resources.md new file mode 100644 index 0000000..78edb92 --- /dev/null +++ b/x-tba/tictactoe/06-refactor/resources.md @@ -0,0 +1,5 @@ +https://github.com/mattn/go-runewidth + +https://github.com/olekukonko/tablewriter + +https://www.cl.cam.ac.uk/~mgk25/ucs/wcwidth.c diff --git a/x-tba/tictactoe/06-refactor/skins.go b/x-tba/tictactoe/06-refactor/skins.go new file mode 100644 index 0000000..a14ee04 --- /dev/null +++ b/x-tba/tictactoe/06-refactor/skins.go @@ -0,0 +1,91 @@ +package main + +import "github.com/fatih/color" + +// skin options :-) +type skin struct { + empty, mark1, mark2 string + header, middle, footer, separator string + + unicode bool +} + +var skins = map[string]skin{ + "colorful": colorfulSkin, + "poseidon": poseidonSkin, + "statues": statuesSkin, + "aliens": aliensSkin, + "snow": snowSkin, + "monkeys": monkeysSkin, +} + +var defaultSkin = skin{ + empty: " ", + mark1: " X ", + mark2: " O ", + + header: "┌───┬───┬───┐", + middle: "├───┼───┼───┤", + footer: "└───┴───┴───┘", + separator: "│", +} + +var colorfulSkin = skin{ + empty: " ", + mark1: color.CyanString(" X "), + mark2: color.HiMagentaString(" O "), + header: color.HiBlueString("┌───┬───┬───┐"), + middle: color.HiBlueString("├───┼───┼───┤"), + footer: color.HiBlueString("└───┴───┴───┘"), + separator: color.BlueString("│"), +} + +var poseidonSkin = skin{ + empty: "❓ ", + mark1: "🔱 ", + mark2: "⚓️ ", + header: "●————●————●————●", + middle: "●————●————●————●", + footer: "●————●————●————●", + separator: "⎮ ", +} + +var statuesSkin = skin{ + empty: "❓ ", + mark1: "🗿 ", + mark2: "🗽 ", + header: "┌────┬────┬────┐", + middle: "├────┼────┼────┤", + footer: "└────┴────┴────┘", + separator: "│ ", +} + +var aliensSkin = skin{ + empty: "❓ ", + mark1: "👽 ", + mark2: "👾 ", + header: "┌────┬────┬────┐", + middle: "├────┼────┼────┤", + footer: "└────┴────┴────┘", + separator: "│ ", +} + +var snowSkin = skin{ + empty: "❓ ", + mark1: "⛄ ️ ", + mark2: "❄️ ", + header: "╔════╦════╦════╗", + middle: "╠════╬════╬════╣", + footer: "╚════╩════╩════╝", + separator: "║ ", +} + +var monkeysSkin = skin{ + empty: "🍌 ", + mark1: "🙈 ", + mark2: "🙉 ", + header: "┌────┬────┬────┐", + middle: "├────┼────┼────┤", + footer: "└────┴────┴────┘", + separator: "│ ", +} diff --git a/x-tba/tictactoe/README.md b/x-tba/tictactoe/README.md new file mode 100644 index 0000000..97c23f7 --- /dev/null +++ b/x-tba/tictactoe/README.md @@ -0,0 +1,3 @@ +**Check out:** +Tic-Tac-Toe Gist: +https://gist.github.com/agalal/bff3cb779c337274d2a3462c630f4d74 \ No newline at end of file diff --git a/x-tba/xxx-wizards-structs/marshal/main.go b/x-tba/wizards-structs/marshal/main.go similarity index 100% rename from x-tba/xxx-wizards-structs/marshal/main.go rename to x-tba/wizards-structs/marshal/main.go diff --git a/x-tba/xxx-wizards-structs/server/list.tmpl.html b/x-tba/wizards-structs/server/list.tmpl.html similarity index 100% rename from x-tba/xxx-wizards-structs/server/list.tmpl.html rename to x-tba/wizards-structs/server/list.tmpl.html diff --git a/x-tba/xxx-wizards-structs/server/main.go b/x-tba/wizards-structs/server/main.go similarity index 100% rename from x-tba/xxx-wizards-structs/server/main.go rename to x-tba/wizards-structs/server/main.go diff --git a/x-tba/xxx-wizards-structs/server/store.go b/x-tba/wizards-structs/server/store.go similarity index 100% rename from x-tba/xxx-wizards-structs/server/store.go rename to x-tba/wizards-structs/server/store.go diff --git a/x-tba/xxx-wizards-structs/server/templates.go b/x-tba/wizards-structs/server/templates.go similarity index 100% rename from x-tba/xxx-wizards-structs/server/templates.go rename to x-tba/wizards-structs/server/templates.go diff --git a/x-tba/xxx-wizards-structs/unmarshal/main.go b/x-tba/wizards-structs/unmarshal/main.go similarity index 100% rename from x-tba/xxx-wizards-structs/unmarshal/main.go rename to x-tba/wizards-structs/unmarshal/main.go