commit bbb948328eb10e1104f9a36e367064faf940bc29 Author: Mischa Taylor Date: Sat Jun 8 15:17:03 2013 -0700 Initial Commit diff --git a/LICENSE-2.0.txt b/LICENSE-2.0.txt new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/LICENSE-2.0.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.txt b/README.txt new file mode 100644 index 0000000..0eedf9d --- /dev/null +++ b/README.txt @@ -0,0 +1,132 @@ +Programming in Go by Mark Summerfield + +ISBN: 0321774639 + +Copyright © 2011-12 Qtrac Ltd. + +All the programs, packages, and associated files in this archive are +licensed under the Apache License, Version 2.0 (the "License"); you may +not use these files except in compliance with the License. You can get a +copy of the License at: http://www.apache.org/licenses/LICENSE-2.0. (The +License is also included in this archive in file LICENSE-2.0.txt.) + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +All the book's examples are designed to be educational, and many are +also designed to be useful. I hope that you find them helpful, and are +perhaps able to use some of them as starting points for your own +projects. + +On Unix-like systems (e.g., Linux, FreeBSD, Mac OS X), once you have +installed Go, you can build all the examples in one go by executing: + + $ cd $HOME/goeg + $ ./build.sh + +On Windows it works similarly: + + C:\>cd goeg + C:\goeg>build.bat + +The build.sh (Unix) or build.bat (Windows) script sets GOPATH +temporarily just for the build and uses the go command (go build); both +assume that the go command (i.e., Go's bin directory) is in the PATH +which it will be if you installed a binary version. + +If you want to build the examples individually and build your own Go +programs you will need to set GOPATH. This can be done temporarily by +running the accompanying gopath.sh (Unix) or gopath.bat (Windows) script +(after editing to change any paths to match your setup), or permanently +by adding the export lines from gopath.sh to your .bashrc file or on +Windows by creating a Go-specific console shortcut: see +gopath.sh or gopath.bat for more information. + +Here is the list of programs and packages referred to in the book +grouped by chapter: + +Chapter 1: An Overview in Five Examples + hello + bigdigits + stack + americanize + polar2cartesian + bigdigits_ans + +Chapter 2: Identifiers, Booleans, and Numbers + pi_by_digits + statistics + statistics_ans + quadratic_ans1 + quadratic_ans2 + +Chapter 3: Strings + m3u2pls + playlist + soundex + +Chapter 4: Collection Types + guess_separator + wordfrequency + chap4_ans + +Chapter 5: Procedural Programming + archive_file_list + archive_file_list_ans + statistics_nonstop + statistics_nonstop2 + contains + palindrome + palindrome_ans + memoize + indent_sort + common_prefix + +Chapter 6: Object-Oriented Programming + fuzzy + fuzzy_immutable + fuzzy_mutable + fuzzy_value + shaper1 + shaper2 + shaper3 + ordered_map + qtrac.eu/omap + font + shaper_ans1 + shaper_ans2 + shaper_ans3 + +Chapter 7: Concurrent Programming + filter + cgrep1 + cgrep2 + cgrep3 + safemap + apachereport1 + apachereport2 + apachereport3 + findduplicates + safeslice + apachereport4 + [apachereport5 added to examples after publication; see errata] + imagetag1 + imagetag2 + sizeimages1 + sizeimages2 + +Chapter 8: File Handling + invoicedata + pack + unpack + unpack_ans + utf16-to-utf8 + invoicedata_ans + +Chapter 9: Packages + qtrac.eu/omap + cgrep3 + linkcheck diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..ac3ccd6 --- /dev/null +++ b/build.sh @@ -0,0 +1,74 @@ +#!/bin/sh +export GOPATH=`pwd` +cd src +DIRS="americanise + apachereport1 + apachereport2 + apachereport3 + apachereport4 + apachereport5 + archive_file_list + archive_file_list_ans + bigdigits + bigdigits_ans + cgrep1 + cgrep2 + cgrep3 + chap4_ans + common_prefix + contains + filter + findduplicates + font + fuzzy + fuzzy_immutable + fuzzy_mutable + fuzzy_value + guess_separator + hello + imagetag1 + imagetag2 + indent_sort + invoicedata + invoicedata_ans + linkcheck + m3u2pls + memoize + ordered_map + pack + palindrome + palindrome_ans + pi_by_digits + playlist + polar2cartesian + quadratic_ans1 + quadratic_ans2 + safemap + safeslice + shaper1 + shaper2 + shaper3 + shaper_ans1 + shaper_ans2 + shaper_ans3 + sizeimages1 + sizeimages2 + soundex + stacker + statistics + statistics_ans + statistics_nonstop + statistics_nonstop2 + wordfrequency + unpack + unpack_ans + utf16-to-utf8" + +for dir in $DIRS +do + cd $dir + echo building $dir... + rm -f $dir + go build + cd .. +done diff --git a/gopath.sh b/gopath.sh new file mode 100755 index 0000000..1a1a3a3 --- /dev/null +++ b/gopath.sh @@ -0,0 +1,20 @@ +#!/bin/sh +# Programming in Go by Mark Summerfield ISBN: 0321774639 + +# Execute this shell script in a console before using the Go tools. +# Or, if you want to be able to use the tools at anytime, copy the +# uncommented export lines into your .bashrc file. + +# Uncomment the following two exports if you installed and built from +# source instead of installing a binary package, and change the path if +# necessary. +#export GOROOT=/usr/local/go +#export PATH=$PATH:$GOROOT/bin + +# Tell your shell where to find the Go book's examples ($HOME/goeg) and +# your own code ($HOME/app/go); change the paths if necessary. If you +# don't need the book's examples anymore just change it to have a single +# path. Important: wherever you put your own Go programs must have a src +# directory, e.g., $HOME/app/go/src, with your programs and packages +# inside it, e.g., $HOME/app/go/src/myapp. +export GOPATH=$HOME/app/go:$HOME/goeg diff --git a/src/americanise/americanise.go b/src/americanise/americanise.go new file mode 100644 index 0000000..1d6e43f --- /dev/null +++ b/src/americanise/americanise.go @@ -0,0 +1,133 @@ +// Copyright © 2010-12 Qtrac Ltd. +// +// This program or package and any associated files are licensed under the +// Apache License, Version 2.0 (the "License"); you may not use these files +// except in compliance with the License. You can get a copy of the License +// at: http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "bufio" + "fmt" + "io" + "io/ioutil" + "log" + "os" + "path/filepath" + "regexp" + "strings" +) + +var britishAmerican = "british-american.txt" + +func init() { + dir, _ := filepath.Split(os.Args[0]) + britishAmerican = filepath.Join(dir, britishAmerican) +} + +func main() { + inFilename, outFilename, err := filenamesFromCommandLine() + if err != nil { + fmt.Println(err) + os.Exit(1) + } + inFile, outFile := os.Stdin, os.Stdout + if inFilename != "" { + if inFile, err = os.Open(inFilename); err != nil { + log.Fatal(err) + } + defer inFile.Close() + } + if outFilename != "" { + if outFile, err = os.Create(outFilename); err != nil { + log.Fatal(err) + } + defer outFile.Close() + } + + if err = americanise(inFile, outFile); err != nil { + log.Fatal(err) + } +} + +func filenamesFromCommandLine() (inFilename, outFilename string, + err error) { + if len(os.Args) > 1 && (os.Args[1] == "-h" || os.Args[1] == "--help") { + err = fmt.Errorf("usage: %s [<]infile.txt [>]outfile.txt", + filepath.Base(os.Args[0])) + return "", "", err + } + if len(os.Args) > 1 { + inFilename = os.Args[1] + if len(os.Args) > 2 { + outFilename = os.Args[2] + } + } + if inFilename != "" && inFilename == outFilename { + log.Fatal("won't overwrite the infile") + } + return inFilename, outFilename, nil +} + +func americanise(inFile io.Reader, outFile io.Writer) (err error) { + reader := bufio.NewReader(inFile) + writer := bufio.NewWriter(outFile) + defer func() { + if err == nil { + err = writer.Flush() + } + }() + + var replacer func(string) string + if replacer, err = makeReplacerFunction(britishAmerican); err != nil { + return err + } + wordRx := regexp.MustCompile("[A-Za-z]+") + eof := false + for !eof { + var line string + line, err = reader.ReadString('\n') + if err == io.EOF { + err = nil // io.EOF isn't really an error + eof = true // this will end the loop at the next iteration + } else if err != nil { + return err // finish immediately for real errors + } + line = wordRx.ReplaceAllStringFunc(line, replacer) + if _, err = writer.WriteString(line); err != nil { + return err + } + } + return nil +} + +func makeReplacerFunction(file string) (func(string) string, error) { + rawBytes, err := ioutil.ReadFile(file) + if err != nil { + return nil, err + } + text := string(rawBytes) + + usForBritish := make(map[string]string) + lines := strings.Split(text, "\n") + for _, line := range lines { + fields := strings.Fields(line) + if len(fields) == 2 { + usForBritish[fields[0]] = fields[1] + } + } + + return func(word string) string { + if usWord, found := usForBritish[word]; found { + return usWord + } + return word + }, nil +} diff --git a/src/americanise/americanise_test.go b/src/americanise/americanise_test.go new file mode 100644 index 0000000..43a8c00 --- /dev/null +++ b/src/americanise/americanise_test.go @@ -0,0 +1,70 @@ +// Copyright © 2011-12 Qtrac Ltd. +// +// This program or package and any associated files are licensed under the +// Apache License, Version 2.0 (the "License"); you may not use these files +// except in compliance with the License. You can get a copy of the License +// at: http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "bytes" + "io/ioutil" + "log" + "os" + "path/filepath" + "testing" +) + +const ( + inFilename = "input.txt" + expectedFilename = "expected.txt" + actualFilename = "actual.txt" +) + +func TestAmericanize(t *testing.T) { + log.SetFlags(0) + log.Println("TEST americanize") + + path, _ := filepath.Split(os.Args[0]) + var inFile, outFile *os.File + var err error + + inFilename := filepath.Join(path, inFilename) + if inFile, err = os.Open(inFilename); err != nil { + t.Fatal(err) + } + defer inFile.Close() + + outFilename := filepath.Join(path, actualFilename) + if outFile, err = os.Create(outFilename); err != nil { + t.Fatal(err) + } + defer outFile.Close() + defer os.Remove(outFilename) + + if err := americanise(inFile, outFile); err != nil { + t.Fatal(err) + } + + compare(outFilename, filepath.Join(path, expectedFilename), t) +} + +func compare(actual, expected string, t *testing.T) { + + if actualBytes, err := ioutil.ReadFile(actual); err != nil { + t.Fatal(err) + } else if expectedBytes, err := ioutil.ReadFile(expected); err != nil { + t.Fatal(err) + } else { + if bytes.Compare(actualBytes, expectedBytes) != 0 { + t.Fatal("actual != expected") + } + } +} diff --git a/src/americanise/british-american.txt b/src/americanise/british-american.txt new file mode 100644 index 0000000..1db9ceb --- /dev/null +++ b/src/americanise/british-american.txt @@ -0,0 +1,62 @@ +behaviour behavior +colour color +favourite favorite +humour humor +labour labor +neighbour neighbor +centre center +fibre fiber +theatre theater +litre liter +metre meter +analogue analog +catalogue catalog +dialogue dialog +anaemia anemia +anaesthesia anesthesia +diarrhoea diarrhea +foetus fetus +haemorrhage hemorrhage +defence defense +offence offense +pretence pretense +aluminium aluminum +cheque check +jewellery jewelry +kerb curb +manoeuvre maneuver +mould mold +plough plow +tyre tire +sulphur sulfur +Behaviour Behavior +Colour Color +Favourite Favorite +Humour Humor +Labour Labor +Neighbour Neighbor +Centre Center +Fibre Fiber +Theatre Theater +Litre Liter +Metre Meter +Analogue Analog +Catalogue Catalog +Dialogue Dialog +Anaemia Anemia +Anaesthesia Anesthesia +Diarrhoea Diarrhea +Foetus Fetus +Haemorrhage Hemorrhage +Defence Defense +Offence Offense +Pretence Pretense +Aluminium Aluminum +Cheque Check +Jewellery Jewelry +Kerb Curb +Manoeuvre Maneuver +Mould Mold +Plough Plow +Tyre Tire +Sulphur Sulfur diff --git a/src/americanise/expected.txt b/src/americanise/expected.txt new file mode 100644 index 0000000..4fb55cf --- /dev/null +++ b/src/americanise/expected.txt @@ -0,0 +1,14 @@ +This is just some test text. It contains some of the British words that +have different spellings in American English, such as behavior, color, +favorite, labor, and neighbor. + +In fact the differences between British and American spellings aren't +that great. Some British words that end with "our" get "or" endings in +American English, and similarly for "re" to "er" (for example "Center" +becomes "Center"), and "ogue" becomes "og" as with dialog and +catalog. + +There are also a few more obscure changes, such as "ence" to "ense", for +example "defense" becomes "defense". And there are a few miscellaneous +differences, such as for anemia, hemorrhage, aluminum, check, curb, +tire, sulfur, and maneuver. diff --git a/src/americanise/input.txt b/src/americanise/input.txt new file mode 100644 index 0000000..2ccee3b --- /dev/null +++ b/src/americanise/input.txt @@ -0,0 +1,14 @@ +This is just some test text. It contains some of the British words that +have different spellings in American English, such as behaviour, colour, +favourite, labour, and neighbour. + +In fact the differences between British and American spellings aren't +that great. Some British words that end with "our" get "or" endings in +American English, and similarly for "re" to "er" (for example "Centre" +becomes "Center"), and "ogue" becomes "og" as with dialogue and +catalogue. + +There are also a few more obscure changes, such as "ence" to "ense", for +example "defence" becomes "defense". And there are a few miscellaneous +differences, such as for anaemia, haemorrhage, aluminium, cheque, kerb, +tyre, sulphur, and manoeuvre. diff --git a/src/apachereport1/apachereport.go b/src/apachereport1/apachereport.go new file mode 100644 index 0000000..cb85b8e --- /dev/null +++ b/src/apachereport1/apachereport.go @@ -0,0 +1,100 @@ +// Copyright © 2011-12 Qtrac Ltd. +// +// This program or package and any associated files are licensed under the +// Apache License, Version 2.0 (the "License"); you may not use these files +// except in compliance with the License. You can get a copy of the License +// at: http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "bufio" + "fmt" + "io" + "log" + "os" + "path/filepath" + "regexp" + "runtime" + "safemap" +) + +var workers = runtime.NumCPU() + +func main() { + runtime.GOMAXPROCS(runtime.NumCPU()) // Use all the machine's cores + if len(os.Args) != 2 || os.Args[1] == "-h" || os.Args[1] == "--help" { + fmt.Printf("usage: %s \n", filepath.Base(os.Args[0])) + os.Exit(1) + } + lines := make(chan string, workers*4) + done := make(chan struct{}, workers) + pageMap := safemap.New() + go readLines(os.Args[1], lines) + processLines(done, pageMap, lines) + waitUntil(done) + showResults(pageMap) +} + +func readLines(filename string, lines chan<- string) { + file, err := os.Open(filename) + if err != nil { + log.Fatal("failed to open the file:", err) + } + defer file.Close() + reader := bufio.NewReader(file) + for { + line, err := reader.ReadString('\n') + if line != "" { + lines <- line + } + if err != nil { + if err != io.EOF { + log.Println("failed to finish reading the file:", err) + } + break + } + } + close(lines) +} + +func processLines(done chan<- struct{}, pageMap safemap.SafeMap, + lines <-chan string) { + getRx := regexp.MustCompile(`GET[ \t]+([^ \t\n]+[.]html?)`) + incrementer := func(value interface{}, found bool) interface{} { + if found { + return value.(int) + 1 + } + return 1 + } + for i := 0; i < workers; i++ { + go func() { + for line := range lines { + if matches := getRx.FindStringSubmatch(line); + matches != nil { + pageMap.Update(matches[1], incrementer) + } + } + done <- struct{}{} + }() + } +} + +func waitUntil(done <-chan struct{}) { + for i := 0; i < workers; i++ { + <-done + } +} + +func showResults(pageMap safemap.SafeMap) { + pages := pageMap.Close() + for page, count := range pages { + fmt.Printf("%8d %s\n", count, page) + } +} diff --git a/src/apachereport2/apachereport.go b/src/apachereport2/apachereport.go new file mode 100644 index 0000000..bfce0ec --- /dev/null +++ b/src/apachereport2/apachereport.go @@ -0,0 +1,112 @@ +// Copyright © 2011-12 Qtrac Ltd. +// +// This program or package and any associated files are licensed under the +// Apache License, Version 2.0 (the "License"); you may not use these files +// except in compliance with the License. You can get a copy of the License +// at: http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "bufio" + "fmt" + "io" + "log" + "os" + "path/filepath" + "regexp" + "runtime" + "sync" +) + +var workers = runtime.NumCPU() + +type pageMap struct { + countForPage map[string]int + mutex *sync.RWMutex +} + +func NewPageMap() *pageMap { + return &pageMap{make(map[string]int), new(sync.RWMutex)} +} + +func (pm *pageMap) Increment(page string) { + pm.mutex.Lock() + defer pm.mutex.Unlock() + pm.countForPage[page]++ +} + +func (pm *pageMap) Len() int { + pm.mutex.RLock() + defer pm.mutex.RUnlock() + return len(pm.countForPage) +} + +func main() { + runtime.GOMAXPROCS(runtime.NumCPU()) // Use all the machine's cores + if len(os.Args) != 2 || os.Args[1] == "-h" || os.Args[1] == "--help" { + fmt.Printf("usage: %s \n", filepath.Base(os.Args[0])) + os.Exit(1) + } + lines := make(chan string, workers*4) + done := make(chan struct{}, workers) + pageMap := NewPageMap() + go readLines(os.Args[1], lines) + getRx := regexp.MustCompile(`GET[ \t]+([^ \t\n]+[.]html?)`) + for i := 0; i < workers; i++ { + go processLines(done, getRx, pageMap, lines) + } + waitUntil(done) + showResults(pageMap) +} + +func readLines(filename string, lines chan<- string) { + file, err := os.Open(filename) + if err != nil { + log.Fatal("failed to open the file:", err) + } + defer file.Close() + reader := bufio.NewReader(file) + for { + line, err := reader.ReadString('\n') + if line != "" { + lines <- line + } + if err != nil { + if err != io.EOF { + log.Println("failed to finish reading the file:", err) + } + break + } + } + close(lines) +} + +func processLines(done chan<- struct{}, getRx *regexp.Regexp, + pageMap *pageMap, lines <-chan string) { + for line := range lines { + if matches := getRx.FindStringSubmatch(line); matches != nil { + pageMap.Increment(matches[1]) + } + } + done <- struct{}{} +} + +func waitUntil(done <-chan struct{}) { + for i := 0; i < workers; i++ { + <-done + } +} + +func showResults(pageMap *pageMap) { + // No lock, accesses in only one goroutine + for page, count := range pageMap.countForPage { + fmt.Printf("%8d %s\n", count, page) + } +} diff --git a/src/apachereport3/apachereport.go b/src/apachereport3/apachereport.go new file mode 100644 index 0000000..f37ff5f --- /dev/null +++ b/src/apachereport3/apachereport.go @@ -0,0 +1,93 @@ +// Copyright © 2011-12 Qtrac Ltd. +// +// This program or package and any associated files are licensed under the +// Apache License, Version 2.0 (the "License"); you may not use these files +// except in compliance with the License. You can get a copy of the License +// at: http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "bufio" + "fmt" + "io" + "log" + "os" + "path/filepath" + "regexp" + "runtime" +) + +var workers = runtime.NumCPU() + +func main() { + runtime.GOMAXPROCS(runtime.NumCPU()) // Use all the machine's cores + if len(os.Args) != 2 || os.Args[1] == "-h" || os.Args[1] == "--help" { + fmt.Printf("usage: %s \n", filepath.Base(os.Args[0])) + os.Exit(1) + } + lines := make(chan string, workers*4) + results := make(chan map[string]int, workers) + go readLines(os.Args[1], lines) + getRx := regexp.MustCompile(`GET[ \t]+([^ \t\n]+[.]html?)`) + for i := 0; i < workers; i++ { + go processLines(results, getRx, lines) + } + totalForPage := make(map[string]int) + merge(results, totalForPage) + showResults(totalForPage) +} + +func readLines(filename string, lines chan<- string) { + file, err := os.Open(filename) + if err != nil { + log.Fatal("failed to open the file:", err) + } + defer file.Close() + reader := bufio.NewReader(file) + for { + line, err := reader.ReadString('\n') + if line != "" { + lines <- line + } + if err != nil { + if err != io.EOF { + log.Println("failed to finish reading the file:", err) + } + break + } + } + close(lines) +} + +func processLines(results chan<- map[string]int, getRx *regexp.Regexp, + lines <-chan string) { + countForPage := make(map[string]int) + for line := range lines { + if matches := getRx.FindStringSubmatch(line); matches != nil { + countForPage[matches[1]]++ + } + } + results <- countForPage +} + +func merge(results <-chan map[string]int, totalForPage map[string]int) { + for i := 0; i < workers; i++ { + countForPage := <-results + for page, count := range countForPage { + totalForPage[page] += count + } + } +} + +func showResults(totalForPage map[string]int) { + for page, count := range totalForPage { + fmt.Printf("%8d %s\n", count, page) + } +} diff --git a/src/apachereport4/apachereport.go b/src/apachereport4/apachereport.go new file mode 100644 index 0000000..b74d3d8 --- /dev/null +++ b/src/apachereport4/apachereport.go @@ -0,0 +1,96 @@ +// Copyright © 2011-12 Qtrac Ltd. +// +// This program or package and any associated files are licensed under the +// Apache License, Version 2.0 (the "License"); you may not use these files +// except in compliance with the License. You can get a copy of the License +// at: http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "bufio" + "fmt" + "io" + "log" + "os" + "path/filepath" + "regexp" + "runtime" + "safeslice" +) + +var workers = runtime.NumCPU() + +func main() { + runtime.GOMAXPROCS(runtime.NumCPU()) // Use all the machine's cores + if len(os.Args) != 2 || os.Args[1] == "-h" || os.Args[1] == "--help" { + fmt.Printf("usage: %s \n", filepath.Base(os.Args[0])) + os.Exit(1) + } + lines := make(chan string) + done := make(chan struct{}, workers) + pageList := safeslice.New() + go readLines(os.Args[1], lines) + getRx := regexp.MustCompile(`GET[ \t]+([^ \t\n]+[.]html?)`) + for i := 0; i < workers; i++ { + go processLines(done, getRx, pageList, lines) + } + waitUntil(done) + showResults(pageList) +} + +func readLines(filename string, lines chan<- string) { + var file *os.File + var err error + if file, err = os.Open(filename); err != nil { + log.Fatal("failed to open the file:", err) + } + defer file.Close() + reader := bufio.NewReader(file) + for { + line, err := reader.ReadString('\n') + if line != "" { + lines <- line + } + if err != nil { + if err != io.EOF { + log.Println("failed to finish reading the file:", err) + } + break + } + } + close(lines) +} + +func processLines(done chan<- struct{}, getRx *regexp.Regexp, + pageList safeslice.SafeSlice, lines <-chan string) { + for line := range lines { + if matches := getRx.FindStringSubmatch(line); matches != nil { + pageList.Append(matches[1]) + } + } + done <- struct{}{} +} + +func waitUntil(done <-chan struct{}) { + for i := 0; i < workers; i++ { + <-done + } +} + +func showResults(pageList safeslice.SafeSlice) { + list := pageList.Close() + counts := make(map[string]int) + for _, page := range list { // uniquify + counts[page.(string)] += 1 + } + for page, count := range counts { + fmt.Printf("%8d %s\n", count, page) + } +} diff --git a/src/apachereport5/apachereport.go b/src/apachereport5/apachereport.go new file mode 100644 index 0000000..27b7e00 --- /dev/null +++ b/src/apachereport5/apachereport.go @@ -0,0 +1,104 @@ +// Copyright © 2011-12 Qtrac Ltd. +// +// This program or package and any associated files are licensed under the +// Apache License, Version 2.0 (the "License"); you may not use these files +// except in compliance with the License. You can get a copy of the License +// at: http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "bufio" + "fmt" + "io" + "log" + "os" + "path/filepath" + "regexp" + "runtime" +) + +var workers = runtime.NumCPU() + +func main() { + runtime.GOMAXPROCS(runtime.NumCPU()) // Use all the machine's cores + if len(os.Args) != 2 || os.Args[1] == "-h" || os.Args[1] == "--help" { + fmt.Printf("usage: %s \n", filepath.Base(os.Args[0])) + os.Exit(1) + } + filename := os.Args[1] + info, err := os.Stat(filename) + if err != nil { + log.Fatal("failed to open the file:", err) + } + chunkSize := info.Size() / int64(workers) + results := make(chan map[string]int, workers) + getRx := regexp.MustCompile(`GET[ \t]+([^ \t\n]+[.]html?)`) + for i := 0; i < workers; i++ { + offset := int64(i) * chunkSize + if i + 1 == workers { + chunkSize *= 2 // Make sure we read all of the last chunk + } + go processLines(results, getRx, filename, offset, chunkSize) + } + totalForPage := make(map[string]int) + merge(results, totalForPage) + showResults(totalForPage) +} + +func processLines(results chan<- map[string]int, getRx *regexp.Regexp, + filename string, offset, chunkSize int64) { + file, err := os.Open(filename) + if err != nil { + log.Fatal("failed to open the file:", err) + } + defer file.Close() + file.Seek(offset, 0) + var bytesRead int64 + reader := bufio.NewReader(file) + if (offset > 0) { // Find first whole line + line, err := reader.ReadString('\n') + if err != nil { + log.Fatal("failed to read the file:", err) + } + bytesRead = int64(len(line)) + } + countForPage := make(map[string]int) + for bytesRead < chunkSize { + line, err := reader.ReadString('\n') + if line != "" { + bytesRead += int64(len(line)) + if matches := getRx.FindStringSubmatch(line); matches != nil { + countForPage[matches[1]]++ + } + } + if err != nil { + if err != io.EOF { + log.Println("failed to finish reading the file:", err) + } + break + } + } + results <- countForPage +} + +func merge(results <-chan map[string]int, totalForPage map[string]int) { + for i := 0; i < workers; i++ { + countForPage := <-results + for page, count := range countForPage { + totalForPage[page] += count + } + } +} + +func showResults(totalForPage map[string]int) { + for page, count := range totalForPage { + fmt.Printf("%8d %s\n", count, page) + } +} diff --git a/src/archive_file_list/archive_file_list.go b/src/archive_file_list/archive_file_list.go new file mode 100644 index 0000000..eedf985 --- /dev/null +++ b/src/archive_file_list/archive_file_list.go @@ -0,0 +1,219 @@ +// Copyright © 2011-12 Qtrac Ltd. +// +// This program or package and any associated files are licensed under the +// Apache License, Version 2.0 (the "License"); you may not use these files +// except in compliance with the License. You can get a copy of the License +// at: http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "archive/tar" + "archive/zip" + "compress/gzip" + "errors" + "fmt" + "io" + "os" + "path/filepath" + "runtime" + "strings" +) + +var FunctionForSuffix = map[string]func(string) ([]string, error){ + ".gz": GzipFileList, ".tar": TarFileList, ".tar.gz": TarFileList, + ".tgz": TarFileList, ".zip": ZipFileList} + +func main() { + if len(os.Args) == 1 || os.Args[1] == "-h" || os.Args[1] == "--help" { + fmt.Printf("usage: %s archive1 [archive2 [... archiveN]]\n", + filepath.Base(os.Args[0])) + os.Exit(1) + + } + args := commandLineFiles(os.Args[1:]) + archiveFileList := ArchiveFileList + if len(args[0]) == 1 && strings.IndexAny(args[0], "12345") != -1 { + which := args[0][0] + args = args[1:] + switch which { + case '2': + archiveFileList = ArchiveFileList2 + case '3': + archiveFileList = ArchiveFileList3 + case '4': + archiveFileList = ArchiveFileList4 + case '5': + archiveFileList = ArchiveFileListMap + } + } + for _, filename := range args { + fmt.Print(filename) + lines, err := archiveFileList(filename) + if err != nil { + fmt.Println(" ERROR:", err) + } else { + fmt.Println() + for _, line := range lines { + fmt.Println(" ", line) + } + } + } +} + +func commandLineFiles(files []string) []string { + if runtime.GOOS == "windows" { + args := make([]string, 0, len(files)) + for _, name := range files { + if matches, err := filepath.Glob(name); err != nil { + args = append(args, name) // Invalid pattern + } else if matches != nil { // At least one match + args = append(args, matches...) + } + } + return args + } + return files +} + +func ArchiveFileList(file string) ([]string, error) { + if suffix := Suffix(file); suffix == ".gz" { + return GzipFileList(file) + } else if suffix == ".tar" || suffix == ".tar.gz" || suffix == ".tgz" { + return TarFileList(file) + } else if suffix == ".zip" { + return ZipFileList(file) + } + return nil, errors.New("unrecognized archive") +} + +func ArchiveFileList2(file string) ([]string, error) { + switch suffix := Suffix(file); suffix { // Naïve and noncanonical! + case ".gz": + return GzipFileList(file) + case ".tar": + fallthrough + case ".tar.gz": + fallthrough + case ".tgz": + return TarFileList(file) + case ".zip": + return ZipFileList(file) + } + return nil, errors.New("unrecognized archive") +} + +func ArchiveFileList3(file string) ([]string, error) { + switch Suffix(file) { + case ".gz": + return GzipFileList(file) + case ".tar": + fallthrough + case ".tar.gz": + fallthrough + case ".tgz": + return TarFileList(file) + case ".zip": + return ZipFileList(file) + } + return nil, errors.New("unrecognized archive") +} + +func ArchiveFileList4(file string) ([]string, error) { + switch Suffix(file) { // Canonical + case ".gz": + return GzipFileList(file) + case ".tar", ".tar.gz", ".tgz": + return TarFileList(file) + case ".zip": + return ZipFileList(file) + } + return nil, errors.New("unrecognized archive") +} + +func ArchiveFileListMap(file string) ([]string, error) { + if function, ok := FunctionForSuffix[Suffix(file)]; ok { + return function(file) + } + return nil, errors.New("unrecognized archive") +} + +func Suffix(file string) string { + file = strings.ToLower(filepath.Base(file)) + if i := strings.LastIndex(file, "."); i > -1 { + if file[i:] == ".bz2" || file[i:] == ".gz" || file[i:] == ".xz" { + if j := strings.LastIndex(file[:i], "."); + j > -1 && strings.HasPrefix(file[j:], ".tar") { + return file[j:] + } + } + return file[i:] + } + return file +} + +func ZipFileList(filename string) ([]string, error) { + zipReader, err := zip.OpenReader(filename) + if err != nil { + return nil, err + } + defer zipReader.Close() + var files []string + for _, file := range zipReader.File { + files = append(files, file.Name) + } + return files, nil +} + +func GzipFileList(filename string) ([]string, error) { + reader, err := os.Open(filename) + if err != nil { + return nil, err + } + defer reader.Close() + gzipReader, err := gzip.NewReader(reader) + if err != nil { + return nil, err + } + return []string{gzipReader.Header.Name}, nil +} + +func TarFileList(filename string) ([]string, error) { + reader, err := os.Open(filename) + if err != nil { + return nil, err + } + defer reader.Close() + var tarReader *tar.Reader + if strings.HasSuffix(filename, ".gz") || + strings.HasSuffix(filename, ".tgz") { + gzipReader, err := gzip.NewReader(reader) + if err != nil { + return nil, err + } + tarReader = tar.NewReader(gzipReader) + } else { + tarReader = tar.NewReader(reader) + } + var files []string + for { + header, err := tarReader.Next() + if err != nil { + if err == io.EOF { + break + } + return files, err + } + if header == nil { + break + } + files = append(files, header.Name) + } + return files, nil +} diff --git a/src/archive_file_list_ans/archive_file_list.go b/src/archive_file_list_ans/archive_file_list.go new file mode 100644 index 0000000..dde9894 --- /dev/null +++ b/src/archive_file_list_ans/archive_file_list.go @@ -0,0 +1,153 @@ +// Copyright © 2011-12 Qtrac Ltd. +// +// This program or package and any associated files are licensed under the +// Apache License, Version 2.0 (the "License"); you may not use these files +// except in compliance with the License. You can get a copy of the License +// at: http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "archive/tar" + "archive/zip" + "compress/bzip2" + "compress/gzip" + "errors" + "fmt" + "io" + "os" + "path/filepath" + "runtime" + "strings" +) + +var FunctionForSuffix = map[string]func(string) ([]string, error){ + ".gz": GzipFileList, ".tar": TarFileList, ".tar.gz": TarFileList, + ".tar.bz2": TarFileList, ".tgz": TarFileList, ".zip": ZipFileList} + +func main() { + if len(os.Args) == 1 || os.Args[1] == "-h" || os.Args[1] == "--help" { + fmt.Printf("usage: %s archive1 [archive2 [... archiveN]]\n", + filepath.Base(os.Args[0])) + os.Exit(1) + + } + args := commandLineFiles(os.Args[1:]) + for _, filename := range args { + fmt.Print(filename) + lines, err := ArchiveFileList(filename) + if err != nil { + fmt.Println(" ERROR:", err) + } else { + fmt.Println() + for _, line := range lines { + fmt.Println(" ", line) + } + } + } +} + +func commandLineFiles(files []string) []string { + if runtime.GOOS == "windows" { + args := make([]string, 0, len(files)) + for _, name := range files { + if matches, err := filepath.Glob(name); err != nil { + args = append(args, name) // Invalid pattern + } else if matches != nil { // At least one match + args = append(args, matches...) + } + } + return args + } + return files +} + +func ArchiveFileList(file string) ([]string, error) { + if function, ok := FunctionForSuffix[Suffix(file)]; ok { + return function(file) + } + return nil, errors.New("unrecognized archive") +} + +func Suffix(file string) string { + file = strings.ToLower(filepath.Base(file)) + if i := strings.LastIndex(file, "."); i > -1 { + if file[i:] == ".bz2" || file[i:] == ".gz" || file[i:] == ".xz" { + if j := strings.LastIndex(file[:i], "."); + j > -1 && strings.HasPrefix(file[j:], ".tar") { + return file[j:] + } + } + return file[i:] + } + return file +} + +func ZipFileList(filename string) ([]string, error) { + zipReader, err := zip.OpenReader(filename) + if err != nil { + return nil, err + } + defer zipReader.Close() + var files []string + for _, file := range zipReader.File { + files = append(files, file.Name) + } + return files, nil +} + +func GzipFileList(filename string) ([]string, error) { + reader, err := os.Open(filename) + if err != nil { + return nil, err + } + defer reader.Close() + gzipReader, err := gzip.NewReader(reader) + if err != nil { + return nil, err + } + return []string{gzipReader.Header.Name}, nil +} + +func TarFileList(filename string) ([]string, error) { + reader, err := os.Open(filename) + if err != nil { + return nil, err + } + defer reader.Close() + var tarReader *tar.Reader + if strings.HasSuffix(filename, ".gz") || + strings.HasSuffix(filename, ".tgz") { + gzipReader, err := gzip.NewReader(reader) + if err != nil { + return nil, err + } + tarReader = tar.NewReader(gzipReader) + } else if strings.HasSuffix(filename, ".bz2") { + bz2Reader := bzip2.NewReader(reader) + tarReader = tar.NewReader(bz2Reader) + } else { + tarReader = tar.NewReader(reader) + } + var files []string + for { + header, err := tarReader.Next() + if err != nil { + if err == io.EOF { + break + } + return files, err + } + if header == nil { + break + } + files = append(files, header.Name) + } + return files, nil +} diff --git a/src/bigdigits/0123456789.txt b/src/bigdigits/0123456789.txt new file mode 100644 index 0000000..ef08482 --- /dev/null +++ b/src/bigdigits/0123456789.txt @@ -0,0 +1,7 @@ + 000 1 222 333 4 55555 666 77777 888 9999 + 0 0 11 2 2 3 3 44 5 6 7 8 8 9 9 +0 0 1 2 3 4 4 5 6 7 8 8 9 9 +0 0 1 2 33 4 4 555 6666 7 888 9999 +0 0 1 2 3 444444 5 6 6 7 8 8 9 + 0 0 1 2 3 3 4 5 5 6 6 7 8 8 9 + 000 111 22222 333 4 555 666 7 888 9 diff --git a/src/bigdigits/bigdigits.go b/src/bigdigits/bigdigits.go new file mode 100644 index 0000000..8f29f26 --- /dev/null +++ b/src/bigdigits/bigdigits.go @@ -0,0 +1,62 @@ +// Copyright © 2010-12 Qtrac Ltd. +// +// This program or package and any associated files are licensed under the +// Apache License, Version 2.0 (the "License"); you may not use these files +// except in compliance with the License. You can get a copy of the License +// at: http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "fmt" + "log" + "os" + "path/filepath" +) + +func main() { + if len(os.Args) == 1 { + fmt.Printf("usage: %s \n", filepath.Base(os.Args[0])) + os.Exit(1) + } + + stringOfDigits := os.Args[1] + for row := range bigDigits[0] { + line := "" + for column := range stringOfDigits { + digit := stringOfDigits[column] - '0' + if 0 <= digit && digit <= 9 { + line += bigDigits[digit][row] + " " + } else { + log.Fatal("invalid whole number") + } + } + fmt.Println(line) + } +} + +var bigDigits = [][]string{ + {" 000 ", + " 0 0 ", + "0 0", + "0 0", + "0 0", + " 0 0 ", + " 000 "}, + {" 1 ", "11 ", " 1 ", " 1 ", " 1 ", " 1 ", "111"}, + {" 222 ", "2 2", " 2 ", " 2 ", " 2 ", "2 ", "22222"}, + {" 333 ", "3 3", " 3", " 33 ", " 3", "3 3", " 333 "}, + {" 4 ", " 44 ", " 4 4 ", "4 4 ", "444444", " 4 ", + " 4 "}, + {"55555", "5 ", "5 ", " 555 ", " 5", "5 5", " 555 "}, + {" 666 ", "6 ", "6 ", "6666 ", "6 6", "6 6", " 666 "}, + {"77777", " 7", " 7 ", " 7 ", " 7 ", "7 ", "7 "}, + {" 888 ", "8 8", "8 8", " 888 ", "8 8", "8 8", " 888 "}, + {" 9999", "9 9", "9 9", " 9999", " 9", " 9", " 9"}, +} diff --git a/src/bigdigits/bigdigits_test.go b/src/bigdigits/bigdigits_test.go new file mode 100644 index 0000000..7d00fdb --- /dev/null +++ b/src/bigdigits/bigdigits_test.go @@ -0,0 +1,55 @@ +// Copyright © 2011-12 Qtrac Ltd. +// +// This program or package and any associated files are licensed under the +// Apache License, Version 2.0 (the "License"); you may not use these files +// except in compliance with the License. You can get a copy of the License +// at: http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "bytes" + "io/ioutil" + "log" + "os" + "os/exec" + "path/filepath" + "testing" +) + +func TestBigDigits(t *testing.T) { + log.SetFlags(0) + log.Println("TEST bigdigits") + + path, _ := os.Getwd() + expected, err := ioutil.ReadFile(filepath.Join(path, "0123456789.txt")) + if err != nil { + t.Fatal(err) + } + executable := filepath.Join(path, "bigdigits") + reader, writer, err := os.Pipe() + if err != nil { + t.Fatal(err) + } + command := exec.Command(executable, "0123456789") + command.Stdout = writer + err = command.Run() + if err != nil { + t.Fatal(err) + } + writer.Close() + actual, err := ioutil.ReadAll(reader) + if err != nil { + t.Fatal(err) + } + reader.Close() + if bytes.Compare(actual, expected) != 0 { + t.Fatal("actual != expected") + } +} diff --git a/src/bigdigits_ans/0123456789.txt b/src/bigdigits_ans/0123456789.txt new file mode 100644 index 0000000..d0d6c38 --- /dev/null +++ b/src/bigdigits_ans/0123456789.txt @@ -0,0 +1,9 @@ +********************************************************************* + 000 1 222 333 4 55555 666 77777 888 9999 + 0 0 11 2 2 3 3 44 5 6 7 8 8 9 9 +0 0 1 2 3 4 4 5 6 7 8 8 9 9 +0 0 1 2 33 4 4 555 6666 7 888 9999 +0 0 1 2 3 444444 5 6 6 7 8 8 9 + 0 0 1 2 3 3 4 5 5 6 6 7 8 8 9 + 000 111 22222 333 4 555 666 7 888 9 +********************************************************************* diff --git a/src/bigdigits_ans/bigdigits.go b/src/bigdigits_ans/bigdigits.go new file mode 100644 index 0000000..6c1f030 --- /dev/null +++ b/src/bigdigits_ans/bigdigits.go @@ -0,0 +1,79 @@ +// Copyright © 2010-12 Qtrac Ltd. +// +// This program or package and any associated files are licensed under the +// Apache License, Version 2.0 (the "License"); you may not use these files +// except in compliance with the License. You can get a copy of the License +// at: http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "fmt" + "log" + "os" + "path/filepath" + "strings" +) + +func main() { + if len(os.Args) == 1 || os.Args[1] == "-h" || + os.Args[1] == "--help" || + (len(os.Args) == 2 && (os.Args[1] == "-b" || + os.Args[1] == "--bar")) { + fmt.Printf("usage: %s [-b|--bar] \n"+ + "-b --bar draw an underbar and an overbar\n", + filepath.Base(os.Args[0])) + os.Exit(1) + } + + bar := false + var stringOfDigits string + if os.Args[1] == "-b" || os.Args[1] == "--bar" { + bar = true + stringOfDigits = os.Args[2] + } else { + stringOfDigits = os.Args[1] + } + for row := range bigDigits[0] { + line := "" + for column := range stringOfDigits { + digit := stringOfDigits[column] - '0' + if 0 <= digit && digit <= 9 { + line += bigDigits[digit][row] + if column+1 < len(stringOfDigits) { + line += " " + } + } else { + log.Fatal("invalid whole number") + } + } + if bar && row == 0 { + fmt.Println(strings.Repeat("*", len(line))) + } + fmt.Println(line) + if bar && row+1 == len(bigDigits[0]) { + fmt.Println(strings.Repeat("*", len(line))) + } + } +} + +var bigDigits = [][]string{ + {" 000 ", " 0 0 ", "0 0", "0 0", "0 0", " 0 0 ", + " 000 "}, + {" 1 ", "11 ", " 1 ", " 1 ", " 1 ", " 1 ", "111"}, + {" 222 ", "2 2", " 2 ", " 2 ", " 2 ", "2 ", "22222"}, + {" 333 ", "3 3", " 3", " 33 ", " 3", "3 3", " 333 "}, + {" 4 ", " 44 ", " 4 4 ", "4 4 ", "444444", " 4 ", + " 4 "}, + {"55555", "5 ", "5 ", " 555 ", " 5", "5 5", " 555 "}, + {" 666 ", "6 ", "6 ", "6666 ", "6 6", "6 6", " 666 "}, + {"77777", " 7", " 7 ", " 7 ", " 7 ", "7 ", "7 "}, + {" 888 ", "8 8", "8 8", " 888 ", "8 8", "8 8", " 888 "}, + {" 9999", "9 9", "9 9", " 9999", " 9", " 9", " 9"}, +} diff --git a/src/bigdigits_ans/bigdigits_test.go b/src/bigdigits_ans/bigdigits_test.go new file mode 100644 index 0000000..206f10d --- /dev/null +++ b/src/bigdigits_ans/bigdigits_test.go @@ -0,0 +1,55 @@ +// Copyright © 2011-12 Qtrac Ltd. +// +// This program or package and any associated files are licensed under the +// Apache License, Version 2.0 (the "License"); you may not use these files +// except in compliance with the License. You can get a copy of the License +// at: http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "bytes" + "io/ioutil" + "log" + "os" + "os/exec" + "path/filepath" + "testing" +) + +func TestBigDigits(t *testing.T) { + log.SetFlags(0) + log.Println("TEST bigdigits_ans") + + path, _ := os.Getwd() + expected, err := ioutil.ReadFile(filepath.Join(path, "0123456789.txt")) + if err != nil { + t.Fatal(err) + } + executable := filepath.Join(path, "bigdigits_ans") + reader, writer, err := os.Pipe() + if err != nil { + t.Fatal(err) + } + command := exec.Command(executable, "-b", "0123456789") + command.Stdout = writer + err = command.Run() + if err != nil { + t.Fatal(err) + } + writer.Close() + actual, err := ioutil.ReadAll(reader) + if err != nil { + t.Fatal(err) + } + reader.Close() + if bytes.Compare(actual, expected) != 0 { + t.Fatal("actual != expected") + } +} diff --git a/src/cgrep1/cgrep.go b/src/cgrep1/cgrep.go new file mode 100644 index 0000000..438bbdd --- /dev/null +++ b/src/cgrep1/cgrep.go @@ -0,0 +1,143 @@ +// Copyright © 2011-12 Qtrac Ltd. +// +// This program or package and any associated files are licensed under the +// Apache License, Version 2.0 (the "License"); you may not use these files +// except in compliance with the License. You can get a copy of the License +// at: http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// The approach taken here was inspired by an example on the gonuts mailing +// list by Roger Peppe. + +package main + +import ( + "bufio" + "bytes" + "fmt" + "io" + "log" + "os" + "path/filepath" + "regexp" + "runtime" +) + +var workers = runtime.NumCPU() + +type Result struct { + filename string + lino int + line string +} + +type Job struct { + filename string + results chan<- Result +} + +func (job Job) Do(lineRx *regexp.Regexp) { + file, err := os.Open(job.filename) + if err != nil { + log.Printf("error: %s\n", err) + return + } + defer file.Close() + reader := bufio.NewReader(file) + for lino := 1; ; lino++ { + line, err := reader.ReadBytes('\n') + line = bytes.TrimRight(line, "\n\r") + if lineRx.Match(line) { + job.results <- Result{job.filename, lino, string(line)} + } + if err != nil { + if err != io.EOF { + log.Printf("error:%d: %s\n", lino, err) + } + break + } + } +} + +func main() { + runtime.GOMAXPROCS(runtime.NumCPU()) // Use all the machine's cores + if len(os.Args) < 3 || os.Args[1] == "-h" || os.Args[1] == "--help" { + fmt.Printf("usage: %s \n", + filepath.Base(os.Args[0])) + os.Exit(1) + } + if lineRx, err := regexp.Compile(os.Args[1]); err != nil { + log.Fatalf("invalid regexp: %s\n", err) + } else { + grep(lineRx, commandLineFiles(os.Args[2:])) + } +} + +func commandLineFiles(files []string) []string { + if runtime.GOOS == "windows" { + args := make([]string, 0, len(files)) + for _, name := range files { + if matches, err := filepath.Glob(name); err != nil { + args = append(args, name) // Invalid pattern + } else if matches != nil { // At least one match + args = append(args, matches...) + } + } + return args + } + return files +} + +func grep(lineRx *regexp.Regexp, filenames []string) { + jobs := make(chan Job, workers) + results := make(chan Result, minimum(1000, len(filenames))) + done := make(chan struct{}, workers) + + go addJobs(jobs, filenames, results) // Executes in its own goroutine + for i := 0; i < workers; i++ { + go doJobs(done, lineRx, jobs) // Each executes in its own goroutine + } + go awaitCompletion(done, results) // Executes in its own goroutine + processResults(results) // Blocks until the work is done +} + +func addJobs(jobs chan<- Job, filenames []string, results chan<- Result) { + for _, filename := range filenames { + jobs <- Job{filename, results} + } + close(jobs) +} + +func doJobs(done chan<- struct{}, lineRx *regexp.Regexp, jobs <-chan Job) { + for job := range jobs { + job.Do(lineRx) + } + done <- struct{}{} +} + +func awaitCompletion(done <-chan struct{}, results chan Result) { + for i := 0; i < workers; i++ { + <-done + } + close(results) +} + +func processResults(results <-chan Result) { + for result := range results { + fmt.Printf("%s:%d:%s\n", result.filename, result.lino, result.line) + } +} + +func minimum(x int, ys ...int) int { + for _, y := range ys { + if y < x { + x = y + } + } + return x +} diff --git a/src/cgrep2/cgrep.go b/src/cgrep2/cgrep.go new file mode 100644 index 0000000..2d20a34 --- /dev/null +++ b/src/cgrep2/cgrep.go @@ -0,0 +1,151 @@ +// Copyright © 2011-12 Qtrac Ltd. +// +// This program or package and any associated files are licensed under the +// Apache License, Version 2.0 (the "License"); you may not use these files +// except in compliance with the License. You can get a copy of the License +// at: http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// The approach taken here was inspired by an example on the gonuts mailing +// list by Roger Peppe. + +package main + +import ( + "bufio" + "bytes" + "fmt" + "io" + "log" + "os" + "path/filepath" + "regexp" + "runtime" +) + +var workers = runtime.NumCPU() + +type Result struct { + filename string + lino int + line string +} + +type Job struct { + filename string + results chan<- Result +} + +func (job Job) Do(lineRx *regexp.Regexp) { + file, err := os.Open(job.filename) + if err != nil { + log.Printf("error: %s\n", err) + return + } + defer file.Close() + reader := bufio.NewReader(file) + for lino := 1; ; lino++ { + line, err := reader.ReadBytes('\n') + line = bytes.TrimRight(line, "\n\r") + if lineRx.Match(line) { + job.results <- Result{job.filename, lino, string(line)} + } + if err != nil { + if err != io.EOF { + log.Printf("error:%d: %s\n", lino, err) + } + break + } + } +} + +func main() { + runtime.GOMAXPROCS(runtime.NumCPU()) // Use all the machine's cores + if len(os.Args) < 3 || os.Args[1] == "-h" || os.Args[1] == "--help" { + fmt.Printf("usage: %s \n", + filepath.Base(os.Args[0])) + os.Exit(1) + } + if lineRx, err := regexp.Compile(os.Args[1]); err != nil { + log.Fatalf("invalid regexp: %s\n", err) + } else { + grep(lineRx, commandLineFiles(os.Args[2:])) + } +} + +func commandLineFiles(files []string) []string { + if runtime.GOOS == "windows" { + args := make([]string, 0, len(files)) + for _, name := range files { + if matches, err := filepath.Glob(name); err != nil { + args = append(args, name) // Invalid pattern + } else if matches != nil { // At least one match + args = append(args, matches...) + } + } + return args + } + return files +} + +func grep(lineRx *regexp.Regexp, filenames []string) { + jobs := make(chan Job, workers) + results := make(chan Result, minimum(1000, len(filenames))) + done := make(chan struct{}, workers) + + go addJobs(jobs, filenames, results) + for i := 0; i < workers; i++ { + go doJobs(done, lineRx, jobs) + } + waitAndProcessResults(done, results) +} + +func addJobs(jobs chan<- Job, filenames []string, results chan<- Result) { + for _, filename := range filenames { + jobs <- Job{filename, results} + } + close(jobs) +} + +func doJobs(done chan<- struct{}, lineRx *regexp.Regexp, jobs <-chan Job) { + for job := range jobs { + job.Do(lineRx) + } + done <- struct{}{} +} + +func waitAndProcessResults(done <-chan struct{}, results <-chan Result) { + for working := workers; working > 0; { + select { // Blocking + case result := <-results: + fmt.Printf("%s:%d:%s\n", result.filename, result.lino, + result.line) + case <-done: + working-- + } + } +DONE: + for { + select { // Nonblocking + case result := <-results: + fmt.Printf("%s:%d:%s\n", result.filename, result.lino, + result.line) + default: + break DONE + } + } +} + +func minimum(x int, ys ...int) int { + for _, y := range ys { + if y < x { + x = y + } + } + return x +} diff --git a/src/cgrep3/cgrep.go b/src/cgrep3/cgrep.go new file mode 100644 index 0000000..beabd9e --- /dev/null +++ b/src/cgrep3/cgrep.go @@ -0,0 +1,159 @@ +// Copyright © 2011-12 Qtrac Ltd. +// +// This program or package and any associated files are licensed under the +// Apache License, Version 2.0 (the "License"); you may not use these files +// except in compliance with the License. You can get a copy of the License +// at: http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// The approach taken here was inspired by an example on the gonuts mailing +// list by Roger Peppe. + +package main + +import ( + "bufio" + "bytes" + "flag" + "fmt" + "io" + "log" + "os" + "regexp" + "runtime" + "time" +) + +var workers = runtime.NumCPU() + +type Result struct { + filename string + lino int + line string +} + +type Job struct { + filename string + results chan<- Result +} + +func (job Job) Do(lineRx *regexp.Regexp) { + file, err := os.Open(job.filename) + if err != nil { + log.Printf("error: %s\n", err) + return + } + defer file.Close() + reader := bufio.NewReader(file) + for lino := 1; ; lino++ { + line, err := reader.ReadBytes('\n') + line = bytes.TrimRight(line, "\n\r") + if lineRx.Match(line) { + job.results <- Result{job.filename, lino, string(line)} + } + if err != nil { + if err != io.EOF { + log.Printf("error:%d: %s\n", lino, err) + } + break + } + } +} + +func main() { + runtime.GOMAXPROCS(runtime.NumCPU()) // Use all the machine's cores + log.SetFlags(0) + var timeoutOpt *int64 = flag.Int64("timeout", 0, + "seconds (0 means no timeout)") + flag.Parse() + if *timeoutOpt < 0 || *timeoutOpt > 240 { + log.Fatalln("timeout must be in the range [0,240] seconds") + } + args := flag.Args() + if len(args) < 1 { + log.Fatalln("a regexp to match must be specified") + } + pattern := args[0] + files := args[1:] + if len(files) < 1 { + log.Fatalln("must provide at least one filename") + } + if lineRx, err := regexp.Compile(pattern); err != nil { + log.Fatalf("invalid regexp: %s\n", err) + } else { + var timeout int64 = 1e9 * 60 * 10 // 10 minutes! + if *timeoutOpt != 0 { + timeout = *timeoutOpt * 1e9 + } + grep(timeout, lineRx, commandLineFiles(files)) + } +} + +func grep(timeout int64, lineRx *regexp.Regexp, filenames []string) { + jobs := make(chan Job, workers) + results := make(chan Result, minimum(1000, len(filenames))) + done := make(chan struct{}, workers) + + go addJobs(jobs, filenames, results) + for i := 0; i < workers; i++ { + go doJobs(done, lineRx, jobs) + } + waitAndProcessResults(timeout, done, results) +} + +func addJobs(jobs chan<- Job, filenames []string, results chan<- Result) { + for _, filename := range filenames { + jobs <- Job{filename, results} + } + close(jobs) +} + +func doJobs(done chan<- struct{}, lineRx *regexp.Regexp, jobs <-chan Job) { + for job := range jobs { + job.Do(lineRx) + } + done <- struct{}{} +} + +func waitAndProcessResults(timeout int64, done <-chan struct{}, + results <-chan Result) { + finish := time.After(time.Duration(timeout)) + for working := workers; working > 0; { + select { // Blocking + case result := <-results: + fmt.Printf("%s:%d:%s\n", result.filename, result.lino, + result.line) + case <-finish: + fmt.Println("timed out") + return // Time's up so finish with what results there were + case <-done: + working-- + } + } + for { + select { // Nonblocking + case result := <-results: + fmt.Printf("%s:%d:%s\n", result.filename, result.lino, + result.line) + case <-finish: + fmt.Println("timed out") + return // Time's up so finish with what results there were + default: + return + } + } +} + +func minimum(x int, ys ...int) int { + for _, y := range ys { + if y < x { + x = y + } + } + return x +} diff --git a/src/cgrep3/util_darwin.go b/src/cgrep3/util_darwin.go new file mode 100644 index 0000000..2304cb0 --- /dev/null +++ b/src/cgrep3/util_darwin.go @@ -0,0 +1,16 @@ +// Copyright © 2011-12 Qtrac Ltd. +// +// This program or package and any associated files are licensed under the +// Apache License, Version 2.0 (the "License"); you may not use these files +// except in compliance with the License. You can get a copy of the License +// at: http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +func commandLineFiles(files []string) []string { return files } diff --git a/src/cgrep3/util_freebsd.go b/src/cgrep3/util_freebsd.go new file mode 100644 index 0000000..2304cb0 --- /dev/null +++ b/src/cgrep3/util_freebsd.go @@ -0,0 +1,16 @@ +// Copyright © 2011-12 Qtrac Ltd. +// +// This program or package and any associated files are licensed under the +// Apache License, Version 2.0 (the "License"); you may not use these files +// except in compliance with the License. You can get a copy of the License +// at: http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +func commandLineFiles(files []string) []string { return files } diff --git a/src/cgrep3/util_linux.go b/src/cgrep3/util_linux.go new file mode 100644 index 0000000..2304cb0 --- /dev/null +++ b/src/cgrep3/util_linux.go @@ -0,0 +1,16 @@ +// Copyright © 2011-12 Qtrac Ltd. +// +// This program or package and any associated files are licensed under the +// Apache License, Version 2.0 (the "License"); you may not use these files +// except in compliance with the License. You can get a copy of the License +// at: http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +func commandLineFiles(files []string) []string { return files } diff --git a/src/cgrep3/util_windows.go b/src/cgrep3/util_windows.go new file mode 100644 index 0000000..2d23d44 --- /dev/null +++ b/src/cgrep3/util_windows.go @@ -0,0 +1,28 @@ +// Copyright © 2011-12 Qtrac Ltd. +// +// This program or package and any associated files are licensed under the +// Apache License, Version 2.0 (the "License"); you may not use these files +// except in compliance with the License. You can get a copy of the License +// at: http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import "path/filepath" + +func commandLineFiles(files []string) []string { + args := make([]string, 0, len(files)) + for _, name := range files { + if matches, err := filepath.Glob(name); err != nil { + args = append(args, name) // Invalid pattern + } else if matches != nil { // At least one match + args = append(args, matches...) + } + } + return args +} diff --git a/src/chap4_ans/chap4_ans.go b/src/chap4_ans/chap4_ans.go new file mode 100644 index 0000000..07a7233 --- /dev/null +++ b/src/chap4_ans/chap4_ans.go @@ -0,0 +1,151 @@ +// Copyright © 2011-12 Qtrac Ltd. +// +// This program or package and any associated files are licensed under the +// Apache License, Version 2.0 (the "License"); you may not use these files +// except in compliance with the License. You can get a copy of the License +// at: http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "fmt" + "log" + "sort" + "strings" +) + +func main() { + irregularMatrix := [][]int{{1, 2, 3, 4}, + {5, 6, 7, 8}, + {9, 10, 11}, + {12, 13, 14, 15}, + {16, 17, 18, 19, 20}} + fmt.Println("irregular:", irregularMatrix) + slice := Flatten(irregularMatrix) + fmt.Printf("1x%d: %v\n", len(slice), slice) + fmt.Printf(" 3x%d: %v\n", neededRows(slice, 3), Make2D(slice, 3)) + fmt.Printf(" 4x%d: %v\n", neededRows(slice, 4), Make2D(slice, 4)) + fmt.Printf(" 5x%d: %v\n", neededRows(slice, 5), Make2D(slice, 5)) + fmt.Printf(" 6x%d: %v\n", neededRows(slice, 6), Make2D(slice, 6)) + slice = []int{9, 1, 9, 5, 4, 4, 2, 1, 5, 4, 8, 8, 4, 3, 6, 9, 5, 7, 5} + fmt.Println("Original:", slice) + slice = UniqueInts(slice) + fmt.Println("Unique: ", slice) + + iniData := []string{ + "; Cut down copy of Mozilla application.ini file", + "", + "[App]", + "Vendor=Mozilla", + "Name=Iceweasel", + "Profile=mozilla/firefox", + "Version=3.5.16", + "[Gecko]", + "MinVersion=1.9.1", + "MaxVersion=1.9.1.*", + "[XRE]", + "EnableProfileMigrator=0", + "EnableExtensionManager=1", + } + ini := ParseIni(iniData) + PrintIni(ini) +} + +// Minimum result length is len(matrix) + len(matrix[0]); by using append() +// we can cope with irregular matrices whose inner slices are of different +// lengths. +func Flatten(matrix [][]int) []int { + slice := make([]int, 0, len(matrix)+len(matrix[0])) + for _, innerSlice := range matrix { + for _, x := range innerSlice { + slice = append(slice, x) + } + } + return slice +} + +func Make2D(slice []int, columns int) [][]int { + matrix := make([][]int, neededRows(slice, columns)) + for i, x := range slice { + row := i / columns + column := i % columns + if matrix[row] == nil { + matrix[row] = make([]int, columns) + } + matrix[row][column] = x + } + return matrix +} + +func neededRows(slice []int, columns int) int { + rows := len(slice) / columns + if len(slice)%columns != 0 { + rows++ + } + return rows +} + +func UniqueInts(slice []int) []int { + seen := map[int]bool{} // == make(map[int]bool) + unique := []int{} // == make([]int, 0) + for _, x := range slice { + if _, found := seen[x]; !found { + unique = append(unique, x) + seen[x] = true + } + } + return unique +} + +func ParseIni(lines []string) map[string]map[string]string { + const separator = "=" + ini := make(map[string]map[string]string) + group := "General" + for _, line := range lines { + line := strings.TrimSpace(line) + if line == "" || strings.HasPrefix(line, ";") { + continue + } + if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") { + group = line[1 : len(line)-1] + } else if i := strings.Index(line, separator); i > -1 { + key := line[:i] + value := line[i+len(separator):] + if _, found := ini[group]; !found { + ini[group] = make(map[string]string) + } + ini[group][key] = value + } else { + log.Print("failed to parse line:", line) + } + } + return ini +} + +func PrintIni(ini map[string]map[string]string) { + groups := make([]string, 0, len(ini)) + for group := range ini { + groups = append(groups, group) + } + sort.Strings(groups) + for i, group := range groups { + fmt.Printf("[%s]\n", group) + keys := make([]string, 0, len(ini[group])) + for key := range ini[group] { + keys = append(keys, key) + } + sort.Strings(keys) + for _, key := range keys { + fmt.Printf("%s=%s\n", key, ini[group][key]) + } + if i+1 < len(groups) { + fmt.Println() + } + } +} diff --git a/src/common_prefix/common_prefix.go b/src/common_prefix/common_prefix.go new file mode 100644 index 0000000..9d2f5c0 --- /dev/null +++ b/src/common_prefix/common_prefix.go @@ -0,0 +1,107 @@ +// Copyright © 2011-12 Qtrac Ltd. +// +// This program or package and any associated files are licensed under the +// Apache License, Version 2.0 (the "License"); you may not use these files +// except in compliance with the License. You can get a copy of the License +// at: http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "bytes" + "fmt" + "path/filepath" + "strings" +) + +func main() { + testData := [][]string{ + {"/home/user/goeg", "/home/user/goeg/prefix", + "/home/user/goeg/prefix/extra"}, + {"/home/user/goeg", "/home/user/goeg/prefix", + "/home/user/prefix/extra"}, + {"/pecan/π/goeg", "/pecan/π/goeg/prefix", + "/pecan/π/prefix/extra"}, + {"/pecan/π/circle", "/pecan/π/circle/prefix", + "/pecan/π/circle/prefix/extra"}, + {"/home/user/goeg", "/home/users/goeg", + "/home/userspace/goeg"}, + {"/home/user/goeg", "/tmp/user", "/var/log"}, + {"/home/mark/goeg", "/home/user/goeg"}, + {"home/user/goeg", "/tmp/user", "/var/log"}, + } + for _, data := range testData { + fmt.Printf("[") + gap := "" + for _, datum := range data { + fmt.Printf("%s\"%s\"", gap, datum) + gap = " " + } + fmt.Println("]") + cp := CommonPrefix(data) + cpp := CommonPathPrefix(data) + equal := "==" + if cpp != cp { + equal = "!=" + } + fmt.Printf("char ⨉ path prefix: \"%s\" %s \"%s\"\n\n", + cp, equal, cpp) + } +} + +func CommonPrefix(texts []string) string { + components := make([][]rune, len(texts)) + for i, text := range texts { + components[i] = []rune(text) + } + if len(components) == 0 || len(components[0]) == 0 { + return "" + } + var common bytes.Buffer +FINISH: + for column := 0; column < len(components[0]); column++ { + char := components[0][column] + for row := 1; row < len(components); row++ { + if column >= len(components[row]) || + components[row][column] != char { + break FINISH + } + } + common.WriteRune(char) + } + return common.String() +} + +func CommonPathPrefix(paths []string) string { + const separator = string(filepath.Separator) + components := make([][]string, len(paths)) + for i, path := range paths { + components[i] = strings.Split(path, separator) + if strings.HasPrefix(path, separator) { + components[i] = append([]string{separator}, components[i]...) + } + } + if len(components) == 0 || len(components[0]) == 0 { + return "" + } + var common []string +FINISH: + for column := range components[0] { + part := components[0][column] + for row := 1; row < len(components); row++ { + if len(components[row]) == 0 || + column >= len(components[row]) || + components[row][column] != part { + break FINISH + } + } + common = append(common, part) + } + return filepath.Join(common...) +} diff --git a/src/contains/contains.go b/src/contains/contains.go new file mode 100644 index 0000000..b28b206 --- /dev/null +++ b/src/contains/contains.go @@ -0,0 +1,191 @@ +// Copyright © 2011-12 Qtrac Ltd. +// +// This program or package and any associated files are licensed under the +// Apache License, Version 2.0 (the "License"); you may not use these files +// except in compliance with the License. You can get a copy of the License +// at: http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "fmt" + "reflect" +) + +type Slicer interface { + EqualTo(i int, x interface{}) bool + Len() int +} + +type IntSlice []int + +func (slice IntSlice) EqualTo(i int, x interface{}) bool { + return slice[i] == x.(int) +} +func (slice IntSlice) Len() int { return len(slice) } + +func IntIndexSlicer(ints []int, x int) int { + return IndexSlicer(IntSlice(ints), x) +} + +type FloatSlice []float64 + +func (slice FloatSlice) EqualTo(i int, x interface{}) bool { + return slice[i] == x.(float64) +} +func (slice FloatSlice) Len() int { return len(slice) } + +func FloatIndexSlicer(floats []float64, x float64) int { + return IndexSlicer(FloatSlice(floats), x) +} + +type StringSlice []string + +func (slice StringSlice) EqualTo(i int, x interface{}) bool { + return slice[i] == x.(string) +} +func (slice StringSlice) Len() int { return len(slice) } + +func StringIndexSlicer(strs []string, x string) int { + return IndexSlicer(StringSlice(strs), x) +} + +// Returns the index position of x in slice or array xs providing xs's +// items are of the same time as x (integers or strings); returns -1 if x +// isn't in xs. Uses a slow linear search suitable for small amounts of +// unsorted data. +func IndexSlicer(slice Slicer, x interface{}) int { + for i := 0; i < slice.Len(); i++ { + if slice.EqualTo(i, x) { + return i + } + } + return -1 +} + +// Returns true if x is in slice or array xs providing xs's items are of +// the same time as x (integers or strings). Uses the Index() function +// which does a slow linear search suitable for small amounts of unsorted +// data. +func InSlice(xs interface{}, x interface{}) bool { + return Index(xs, x) > -1 +} + +// Returns the index position of x in slice or array xs providing xs's +// items are of the same time as x (integers or strings); returns -1 if x +// isn't in xs. Uses a slow linear search suitable for small amounts of +// unsorted data. +func Index(xs interface{}, x interface{}) int { + switch slice := xs.(type) { + case []int: + for i, y := range slice { + if y == x.(int) { + return i + } + } + case []string: + for i, y := range slice { + if y == x.(string) { + return i + } + } + } + return -1 +} + +func InSliceReflect(xs interface{}, x interface{}) bool { + return IndexReflect(xs, x) > -1 +} + +func IndexReflectX(xs interface{}, x interface{}) int { // Long-winded way + if slice := reflect.ValueOf(xs); slice.Kind() == reflect.Slice { + for i := 0; i < slice.Len(); i++ { + switch y := slice.Index(i).Interface().(type) { + case int: + if y == x.(int) { + return i + } + case string: + if y == x.(string) { + return i + } + } + } + } + return -1 +} + +func IndexReflect(xs interface{}, x interface{}) int { + if slice := reflect.ValueOf(xs); slice.Kind() == reflect.Slice { + for i := 0; i < slice.Len(); i++ { + if reflect.DeepEqual(x, slice.Index(i)) { + return i + } + } + } + return -1 +} + +func IntSliceIndex(xs []int, x int) int { + for i, y := range xs { + if x == y { + return i + } + } + return -1 +} + +func StringSliceIndex(xs []string, s string) int { + for i, x := range xs { + if x == s { + return i + } + } + return -1 +} + +func SliceIndex(limit int, predicate func(i int) bool) int { + for i := 0; i < limit; i++ { + if predicate(i) { + return i + } + } + return -1 +} + +func main() { + xs := []int{2, 4, 6, 8} + fmt.Println("5 @", Index(xs, 5), " 6 @", Index(xs, 6)) + ys := []string{"C", "B", "K", "A"} + fmt.Println("Z @", Index(ys, "Z"), " A @", Index(ys, "A")) + + fmt.Println("5 @", IndexReflectX(xs, 5), " 6 @", IndexReflectX(xs, 6)) + fmt.Println("Z @", IndexReflectX(ys, "Z"), " A @", + IndexReflectX(ys, "A")) + fmt.Println("5 @", IndexReflect(xs, 5), " 6 @", IndexReflect(xs, 6)) + fmt.Println("Z @", IndexReflect(ys, "Z"), " A @", + IndexReflect(ys, "A")) + + fmt.Println("5 @", IntIndexSlicer(xs, 5), + " 6 @", IntIndexSlicer(xs, 6)) + fmt.Println("Z @", StringIndexSlicer(ys, "Z"), + " A @", StringIndexSlicer(ys, "A")) + + sliceIndex() +} + +func sliceIndex() { + xs := []int{2, 4, 6, 8} + ys := []string{"C", "B", "K", "A"} + fmt.Println( + SliceIndex(len(xs), func(i int) bool { return xs[i] == 5 }), + SliceIndex(len(xs), func(i int) bool { return xs[i] == 6 }), + SliceIndex(len(ys), func(i int) bool { return ys[i] == "Z" }), + SliceIndex(len(ys), func(i int) bool { return ys[i] == "A" })) +} diff --git a/src/filter/filter.go b/src/filter/filter.go new file mode 100644 index 0000000..bec31d9 --- /dev/null +++ b/src/filter/filter.go @@ -0,0 +1,127 @@ +// Copyright © 2011-12 Qtrac Ltd. +// +// This program or package and any associated files are licensed under the +// Apache License, Version 2.0 (the "License"); you may not use these files +// except in compliance with the License. You can get a copy of the License +// at: http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "flag" + "fmt" + "log" + "os" + "path/filepath" + "runtime" + "strings" +) + +func main() { + runtime.GOMAXPROCS(runtime.NumCPU()) // Use all the machine's cores + log.SetFlags(0) + algorithm, + minSize, maxSize, suffixes, files := handleCommandLine() + + if algorithm == 1 { + sink(filterSize(minSize, maxSize, filterSuffixes(suffixes, source(files)))) + } else { + channel1 := source(files) + channel2 := filterSuffixes(suffixes, channel1) + channel3 := filterSize(minSize, maxSize, channel2) + sink(channel3) + } +} + +func handleCommandLine() (algorithm int, minSize, maxSize int64, + suffixes, files []string) { + flag.IntVar(&algorithm, "algorithm", 1, "1 or 2") + flag.Int64Var(&minSize, "min", -1, + "minimum file size (-1 means no minimum)") + flag.Int64Var(&maxSize, "max", -1, + "maximum file size (-1 means no maximum)") + var suffixesOpt *string = flag.String("suffixes", "", + "comma-separated list of file suffixes") + flag.Parse() + if algorithm != 1 && algorithm != 2 { + algorithm = 1 + } + if minSize > maxSize && maxSize != -1 { + log.Fatalln("minimum size must be < maximum size") + } + suffixes = []string{} + if *suffixesOpt != "" { + suffixes = strings.Split(*suffixesOpt, ",") + } + files = flag.Args() + return algorithm, minSize, maxSize, suffixes, files +} + +func source(files []string) <-chan string { + out := make(chan string, 1000) + go func() { + for _, filename := range files { + out <- filename + } + close(out) + }() + return out +} + +// make the buffer the same size as for files to maximize throughput +func filterSuffixes(suffixes []string, in <-chan string) <-chan string { + out := make(chan string, cap(in)) + go func() { + for filename := range in { + if len(suffixes) == 0 { + out <- filename + continue + } + ext := strings.ToLower(filepath.Ext(filename)) + for _, suffix := range suffixes { + if ext == suffix { + out <- filename + break + } + } + } + close(out) + }() + return out +} + +// make the buffer the same size as for files to maximize throughput +func filterSize(minimum, maximum int64, in <-chan string) <-chan string { + out := make(chan string, cap(in)) + go func() { + for filename := range in { + if minimum == -1 && maximum == -1 { + out <- filename // don't do a stat call it not needed + continue + } + finfo, err := os.Stat(filename) + if err != nil { + continue // ignore files we can't process + } + size := finfo.Size() + if (minimum == -1 || minimum > -1 && minimum <= size) && + (maximum == -1 || maximum > -1 && maximum >= size) { + out <- filename + } + } + close(out) + }() + return out +} + +func sink(in <-chan string) { + for filename := range in { + fmt.Println(filename) + } +} diff --git a/src/findduplicates/findduplicates.go b/src/findduplicates/findduplicates.go new file mode 100644 index 0000000..8752548 --- /dev/null +++ b/src/findduplicates/findduplicates.go @@ -0,0 +1,149 @@ +// Copyright © 2011-12 Qtrac Ltd. +// +// This program or package and any associated files are licensed under the +// Apache License, Version 2.0 (the "License"); you may not use these files +// except in compliance with the License. You can get a copy of the License +// at: http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Special thanks to Russ Cox for some excellent help with the original +// version of this example. + +package main + +import ( + "crypto/sha1" + "fmt" + "io" + "log" + "os" + "path/filepath" + "runtime" + "sort" + "sync" +) + +const maxSizeOfSmallFile = 1024 * 32 +const maxGoroutines = 100 + +type pathsInfo struct { + size int64 + paths []string +} + +type fileInfo struct { + sha1 []byte + size int64 + path string +} + +func main() { + runtime.GOMAXPROCS(runtime.NumCPU()) // Use all the machine's cores + if len(os.Args) == 1 || os.Args[1] == "-h" || os.Args[1] == "--help" { + fmt.Printf("usage: %s \n", filepath.Base(os.Args[0])) + os.Exit(1) + } + + infoChan := make(chan fileInfo, maxGoroutines*2) + go findDuplicates(infoChan, os.Args[1]) + pathData := mergeResults(infoChan) + outputResults(pathData) +} + +func findDuplicates(infoChan chan fileInfo, dirname string) { + waiter := &sync.WaitGroup{} + filepath.Walk(dirname, makeWalkFunc(infoChan, waiter)) + waiter.Wait() // Blocks until all the work is done + close(infoChan) +} + +func makeWalkFunc(infoChan chan fileInfo, + waiter *sync.WaitGroup) func(string, os.FileInfo, error) error { + return func(path string, info os.FileInfo, err error) error { + if err == nil && info.Size() > 0 && + (info.Mode()&os.ModeType == 0) { + if info.Size() < maxSizeOfSmallFile || + runtime.NumGoroutine() > maxGoroutines { + processFile(path, info, infoChan, nil) + } else { + waiter.Add(1) + go processFile(path, info, infoChan, + func() { waiter.Done() }) + } + } + return nil // We ignore all errors + } +} + +func processFile(filename string, info os.FileInfo, + infoChan chan fileInfo, done func()) { + if done != nil { + defer done() + } + file, err := os.Open(filename) + if err != nil { + log.Println("error:", err) + return + } + defer file.Close() + hash := sha1.New() + if size, err := io.Copy(hash, file); + size != info.Size() || err != nil { + if err != nil { + log.Println("error:", err) + } else { + log.Println("error: failed to read the whole file:", filename) + } + return + } + infoChan <- fileInfo{hash.Sum(nil), info.Size(), filename} +} + +func mergeResults(infoChan <-chan fileInfo) map[string]*pathsInfo { + pathData := make(map[string]*pathsInfo) + format := fmt.Sprintf("%%016X:%%%dX", sha1.Size*2) // == "%016X:%40X" + for info := range infoChan { + key := fmt.Sprintf(format, info.size, info.sha1) + value, found := pathData[key] + if !found { + value = &pathsInfo{size: info.size} + pathData[key] = value + } + value.paths = append(value.paths, info.path) + } + return pathData +} + +func outputResults(pathData map[string]*pathsInfo) { + keys := make([]string, 0, len(pathData)) + for key := range pathData { + keys = append(keys, key) + } + sort.Strings(keys) + for _, key := range keys { + value := pathData[key] + if len(value.paths) > 1 { + fmt.Printf("%d duplicate files (%s bytes):\n", + len(value.paths), commas(value.size)) + sort.Strings(value.paths) + for _, name := range value.paths { + fmt.Printf("\t%s\n", name) + } + } + } +} + +// commas() returns a string representing the whole number with comma +// grouping. +func commas(x int64) string { + value := fmt.Sprint(x) + for i := len(value) - 3; i > 0; i -= 3 { + value = value[:i] + "," + value[i:] + } + return value +} diff --git a/src/font/font.go b/src/font/font.go new file mode 100644 index 0000000..35448cd --- /dev/null +++ b/src/font/font.go @@ -0,0 +1,64 @@ +// Copyright © 2011-12 Qtrac Ltd. +// +// This program or package and any associated files are licensed under the +// Apache License, Version 2.0 (the "License"); you may not use these files +// except in compliance with the License. You can get a copy of the License +// at: http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package font + +import ( + "fmt" + "log" + "unicode/utf8" +) + +type Font struct { + family string + size int +} + +func New(family string, size int) *Font { + return &Font{saneFamily("sans-serif", family), saneSize(10, size)} +} + +func (font *Font) Family() string { return font.family } + +func (font *Font) SetFamily(family string) { + font.family = saneFamily(font.family, family) +} + +func (font *Font) Size() int { return font.size } + +func (font *Font) SetSize(size int) { + font.size = saneSize(font.size, size) +} + +func (font *Font) String() string { + return fmt.Sprintf("{font-family: %q; font-size: %dpt;}", font.family, + font.size) +} + +func saneFamily(oldFamily, newFamily string) string { + if len(newFamily) < utf8.UTFMax && + utf8.RuneCountInString(newFamily) < 1 { + log.Printf("font.saneFamily(): ignored invalid family '%s'", + newFamily) + return oldFamily + } + return newFamily +} + +func saneSize(oldSize, newSize int) int { + if newSize < 5 || newSize > 144 { + log.Printf("font.saneSize(): ignored invalid size '%d'", newSize) + return oldSize + } + return newSize +} diff --git a/src/font/font_test.go b/src/font/font_test.go new file mode 100644 index 0000000..670d861 --- /dev/null +++ b/src/font/font_test.go @@ -0,0 +1,67 @@ +// Copyright © 2011-12 Qtrac Ltd. +// +// This program or package and any associated files are licensed under the +// Apache License, Version 2.0 (the "License"); you may not use these files +// except in compliance with the License. You can get a copy of the License +// at: http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package font_test + +import ( + "fmt" + "font" + "testing" +) + +func TestFont(t *testing.T) { + bodyFont := font.New("Nimbus Sans", 10) + titleFont := font.New("serif", 11) + f1(bodyFont, titleFont, t) +} + +func f1(bodyFont, titleFont *font.Font, t *testing.T) { + if bodyFont.String() != + `{font-family: "Nimbus Sans"; font-size: 10pt;}` { + t.Fatal("#1 bodyFont invalid CSS") + } + if bodyFont.Size() != 10 || bodyFont.Family() != "Nimbus Sans" { + t.Fatal("#2 bodyFont invalid attributes") + } + bodyFont.SetSize(12) + if bodyFont.Size() != 12 || bodyFont.Family() != "Nimbus Sans" { + t.Fatal("#3 bodyFont invalid attributes") + } + if bodyFont.String() != + `{font-family: "Nimbus Sans"; font-size: 12pt;}` { + t.Fatal("#4 bodyFont invalid CSS") + } + bodyFont.SetFamily("") + if bodyFont.Size() != 12 || bodyFont.Family() != "Nimbus Sans" { + t.Fatal("#5 bodyFont invalid attributes") + } + + if titleFont.String() != `{font-family: "serif"; font-size: 11pt;}` { + t.Fatal("#6 titleFont invalid CSS") + } + if titleFont.Size() != 11 || titleFont.Family() != "serif" { + t.Fatal("#7 titleFont invalid attributes") + } + titleFont.SetFamily("Helvetica") + titleFont.SetSize(20) + if titleFont.Size() != 20 || titleFont.Family() != "Helvetica" { + t.Fatal("#8 titleFont invalid attributes") + } + + f2(bodyFont, titleFont) +} + +func f2(bodyFont, titleFont *font.Font) { + fmt.Println(bodyFont) + fmt.Println(titleFont) +} diff --git a/src/fuzzy/fuzzy.go b/src/fuzzy/fuzzy.go new file mode 100644 index 0000000..cfaf7ec --- /dev/null +++ b/src/fuzzy/fuzzy.go @@ -0,0 +1,45 @@ +// Copyright © 2011-12 Qtrac Ltd. +// +// This program or package and any associated files are licensed under the +// Apache License, Version 2.0 (the "License"); you may not use these files +// except in compliance with the License. You can get a copy of the License +// at: http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package main + +import ( + "fmt" + "fuzzy/fuzzybool" +) + +func main() { + a, _ := fuzzybool.New(0) // Safe to ignore err value when using + b, _ := fuzzybool.New(.25) // known valid values; must check if using + c, _ := fuzzybool.New(.75) // variables though. + d := c.Copy() + if err := d.Set(1); err != nil { + fmt.Println(err) + } + process(a, b, c, d) + s := []*fuzzybool.FuzzyBool{a, b, c, d} + fmt.Println(s) +} + +func process(a, b, c, d *fuzzybool.FuzzyBool) { + fmt.Println("Original:", a, b, c, d) + fmt.Println("Not: ", a.Not(), b.Not(), c.Not(), d.Not()) + fmt.Println("Not Not: ", a.Not().Not(), b.Not().Not(), c.Not().Not(), + d.Not().Not()) + fmt.Print("0.And(.25)→", a.And(b), "• .25.And(.75)→", b.And(c), + "• .75.And(1)→", c.And(d), " • .25.And(.75,1)→", b.And(c, d), "\n") + fmt.Print("0.Or(.25)→", a.Or(b), "• .25.Or(.75)→", b.Or(c), + "• .75.Or(1)→", c.Or(d), " • .25.Or(.75,1)→", b.Or(c, d), "\n") + fmt.Println("a < c, a == c, a > c:", a.Less(c), a.Equal(c), c.Less(a)) + fmt.Println("Bool: ", a.Bool(), b.Bool(), c.Bool(), d.Bool()) + fmt.Println("Float: ", a.Float(), b.Float(), c.Float(), d.Float()) +} diff --git a/src/fuzzy/fuzzybool/fuzzybool.go b/src/fuzzy/fuzzybool/fuzzybool.go new file mode 100644 index 0000000..0601942 --- /dev/null +++ b/src/fuzzy/fuzzybool/fuzzybool.go @@ -0,0 +1,104 @@ +// Copyright © 2011-12 Qtrac Ltd. +// +// This program or package and any associated files are licensed under the +// Apache License, Version 2.0 (the "License"); you may not use these files +// except in compliance with the License. You can get a copy of the License +// at: http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package fuzzybool + +import "fmt" + +type FuzzyBool struct{ value float32 } + +func New(value interface{}) (*FuzzyBool, error) { + amount, err := float32ForValue(value) + return &FuzzyBool{amount}, err +} + +func float32ForValue(value interface{}) (fuzzy float32, err error) { + switch value := value.(type) { // shadow variable + case float32: + fuzzy = value + case float64: + fuzzy = float32(value) + case int: + fuzzy = float32(value) + case bool: + fuzzy = 0 + if value { + fuzzy = 1 + } + default: + return 0, fmt.Errorf("float32ForValue(): %v is not a "+ + "number or Boolean", value) + } + if fuzzy < 0 { + fuzzy = 0 + } else if fuzzy > 1 { + fuzzy = 1 + } + return fuzzy, nil +} + +func (fuzzy *FuzzyBool) Set(value interface{}) (err error) { + fuzzy.value, err = float32ForValue(value) + return err +} + +func (fuzzy *FuzzyBool) Copy() *FuzzyBool { + return &FuzzyBool{fuzzy.value} +} + +func (fuzzy *FuzzyBool) String() string { + return fmt.Sprintf("%.0f%%", 100*fuzzy.value) +} + +func (fuzzy *FuzzyBool) Not() *FuzzyBool { + return &FuzzyBool{1 - fuzzy.value} +} + +func (fuzzy *FuzzyBool) And(first *FuzzyBool, + rest ...*FuzzyBool) *FuzzyBool { + minimum := fuzzy.value + rest = append(rest, first) + for _, other := range rest { + if minimum > other.value { + minimum = other.value + } + } + return &FuzzyBool{minimum} +} + +func (fuzzy *FuzzyBool) Or(first *FuzzyBool, + rest ...*FuzzyBool) *FuzzyBool { + maximum := fuzzy.value + rest = append(rest, first) + for _, other := range rest { + if maximum < other.value { + maximum = other.value + } + } + return &FuzzyBool{maximum} +} + +func (fuzzy *FuzzyBool) Less(other *FuzzyBool) bool { + return fuzzy.value < other.value +} + +func (fuzzy *FuzzyBool) Equal(other *FuzzyBool) bool { + return fuzzy.value == other.value +} + +func (fuzzy *FuzzyBool) Bool() bool { + return fuzzy.value >= .5 +} + +func (fuzzy *FuzzyBool) Float() float64 { + return float64(fuzzy.value) +} diff --git a/src/fuzzy_immutable/fuzzybool/fuzzybool.go b/src/fuzzy_immutable/fuzzybool/fuzzybool.go new file mode 100644 index 0000000..655fe28 --- /dev/null +++ b/src/fuzzy_immutable/fuzzybool/fuzzybool.go @@ -0,0 +1,95 @@ +// Copyright © 2011-12 Qtrac Ltd. +// +// This program or package and any associated files are licensed under the +// Apache License, Version 2.0 (the "License"); you may not use these files +// except in compliance with the License. You can get a copy of the License +// at: http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package fuzzybool + +import "fmt" + +type FuzzyBool float32 + +func New(value interface{}) (FuzzyBool, error) { + var fuzzy float32 + switch value := value.(type) { // shadow variable + case float32: + fuzzy = value + case float64: + fuzzy = float32(value) + case int: + fuzzy = float32(value) + case bool: + fuzzy = 0 + if value { + fuzzy = 1 + } + default: + return FuzzyBool(0), fmt.Errorf("fuzzybool.New(): %v is not a " + + "number or boolean\n", value) + } + if fuzzy < 0 { + fuzzy = 0 + } else if fuzzy > 1 { + fuzzy = 1 + } + return FuzzyBool(fuzzy), nil +} + +func (fuzzy FuzzyBool) Copy() FuzzyBool { + return FuzzyBool(fuzzy) +} + +func (fuzzy FuzzyBool) String() string { + return fmt.Sprintf("%.0f%%", 100*float32(fuzzy)) +} + +func (fuzzy FuzzyBool) Not() FuzzyBool { + return FuzzyBool(1 - float32(fuzzy)) +} + +func (fuzzy FuzzyBool) And(first FuzzyBool, + rest ...FuzzyBool) FuzzyBool { + minimum := fuzzy + rest = append(rest, first) + for _, other := range rest { + if minimum > other { + minimum = other + } + } + return FuzzyBool(minimum) +} + +func (fuzzy FuzzyBool) Or(first FuzzyBool, + rest ...FuzzyBool) FuzzyBool { + maximum := fuzzy + rest = append(rest, first) + for _, other := range rest { + if maximum < other { + maximum = other + } + } + return FuzzyBool(maximum) +} + +func (fuzzy FuzzyBool) Less(other FuzzyBool) bool { + return fuzzy < other +} + +func (fuzzy FuzzyBool) Equal(other FuzzyBool) bool { + return fuzzy == other +} + +func (fuzzy FuzzyBool) Bool() bool { + return float32(fuzzy) >= .5 +} + +func (fuzzy FuzzyBool) Float() float64 { + return float64(fuzzy) +} diff --git a/src/fuzzy_immutable/immutable_fuzzy.go b/src/fuzzy_immutable/immutable_fuzzy.go new file mode 100644 index 0000000..ce1f7f5 --- /dev/null +++ b/src/fuzzy_immutable/immutable_fuzzy.go @@ -0,0 +1,44 @@ +// Copyright © 2011-12 Qtrac Ltd. +// +// This program or package and any associated files are licensed under the +// Apache License, Version 2.0 (the "License"); you may not use these files +// except in compliance with the License. You can get a copy of the License +// at: http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package main + +import ( + "fmt" + "fuzzy_immutable/fuzzybool" +) + +func main() { + a, _ := fuzzybool.New(0) + b, _ := fuzzybool.New(.25) + c, _ := fuzzybool.New(.75) + d := c.Copy() + d, _ = fuzzybool.New(1) + process(a, b, c, d) + s := []fuzzybool.FuzzyBool{a, b, c, d} + fmt.Println(s) +} + +func process(a, b, c, d fuzzybool.FuzzyBool) { + fmt.Println("Original:", a, b, c, d) + fmt.Println("Not: ", a.Not(), b.Not(), c.Not(), d.Not()) + fmt.Println("Not Not: ", a.Not().Not(), b.Not().Not(), c.Not().Not(), + d.Not().Not()) + fmt.Print("0.And(.25)→", a.And(b), " .25.And(.75)→", b.And(c), + " .75.And(1)→", c.And(d), " 0.And(.25,.75,1)→", a.And(b, c, d), + "\n") + fmt.Print("0.Or(.25)→", a.Or(b), " .25.Or(.75)→", b.Or(c), + " .75.Or(1)→", c.Or(d), " 0.Or(.25,.75,1)→", a.Or(b, c, d), "\n") + fmt.Println("a < c, a == c, a > c:", a.Less(c), a.Equal(c), c.Less(a)) + fmt.Println("Bool: ", a.Bool(), b.Bool(), c.Bool(), d.Bool()) + fmt.Println("Float: ", a.Float(), b.Float(), c.Float(), d.Float()) +} diff --git a/src/fuzzy_mutable/fuzzybool/fuzzybool.go b/src/fuzzy_mutable/fuzzybool/fuzzybool.go new file mode 100644 index 0000000..d2e3212 --- /dev/null +++ b/src/fuzzy_mutable/fuzzybool/fuzzybool.go @@ -0,0 +1,111 @@ +// Copyright © 2011-12 Qtrac Ltd. +// +// This program or package and any associated files are licensed under the +// Apache License, Version 2.0 (the "License"); you may not use these files +// except in compliance with the License. You can get a copy of the License +// at: http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package fuzzybool + +import "fmt" + +type FuzzyBool float32 + +func New(value interface{}) (*FuzzyBool, error) { + var fuzzy FuzzyBool + result, err := float32ForValue(value) + fuzzy = FuzzyBool(result) + return &fuzzy, err +} + +func float32ForValue(value interface{}) (fuzzy float32, err error) { + switch value := value.(type) { // shadow variable + case float32: + fuzzy = value + case float64: + fuzzy = float32(value) + case int: + fuzzy = float32(value) + case bool: + fuzzy = 0 + if value { + fuzzy = 1 + } + default: + return 0, fmt.Errorf("float32ForValue(): %v is not a "+ + "number or Boolean", value) + } + if fuzzy < 0 { + fuzzy = 0 + } else if fuzzy > 1 { + fuzzy = 1 + } + return fuzzy, nil +} + +func (fuzzy *FuzzyBool) Set(value interface{}) error { + result, err := float32ForValue(value) + *fuzzy = FuzzyBool(result) + return err +} + +func (fuzzy *FuzzyBool) Copy() *FuzzyBool { + result := FuzzyBool(*fuzzy) + return &result +} + +func (fuzzy *FuzzyBool) String() string { + return fmt.Sprintf("%.0f%%", 100*float32(*fuzzy)) +} + +func (fuzzy *FuzzyBool) Not() *FuzzyBool { + result := FuzzyBool(1 - float32(*fuzzy)) + return &result +} + +func (fuzzy *FuzzyBool) And(first *FuzzyBool, + rest ...*FuzzyBool) *FuzzyBool { + minimum := *fuzzy + rest = append(rest, first) + for _, other := range rest { + if minimum > *other { + minimum = *other + } + } + result := FuzzyBool(minimum) + return &result +} + +func (fuzzy *FuzzyBool) Or(first *FuzzyBool, + rest ...*FuzzyBool) *FuzzyBool { + maximum := *fuzzy + rest = append(rest, first) + for _, other := range rest { + if maximum < *other { + maximum = *other + } + } + result := FuzzyBool(maximum) + return &result +} + +func (fuzzy *FuzzyBool) Less(other *FuzzyBool) bool { + return *fuzzy < *other +} + +func (fuzzy *FuzzyBool) Equal(other *FuzzyBool) bool { + return *fuzzy == *other +} + +func (fuzzy *FuzzyBool) Bool() bool { + return float32(*fuzzy) >= .5 +} + +func (fuzzy *FuzzyBool) Float() float64 { + return float64(*fuzzy) +} diff --git a/src/fuzzy_mutable/mutable_fuzzy.go b/src/fuzzy_mutable/mutable_fuzzy.go new file mode 100644 index 0000000..4c04617 --- /dev/null +++ b/src/fuzzy_mutable/mutable_fuzzy.go @@ -0,0 +1,46 @@ +// Copyright © 2011-12 Qtrac Ltd. +// +// This program or package and any associated files are licensed under the +// Apache License, Version 2.0 (the "License"); you may not use these files +// except in compliance with the License. You can get a copy of the License +// at: http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package main + +import ( + "fmt" + "fuzzy_mutable/fuzzybool" +) + +func main() { + a, _ := fuzzybool.New(0) + b, _ := fuzzybool.New(.25) + c, _ := fuzzybool.New(.75) + d := c.Copy() + if err := d.Set(1); err != nil { + fmt.Println(err) + } + process(a, b, c, d) + s := []*fuzzybool.FuzzyBool{a, b, c, d} + fmt.Println(s) +} + +func process(a, b, c, d *fuzzybool.FuzzyBool) { + fmt.Println("Original:", a, b, c, d) + fmt.Println("Not: ", a.Not(), b.Not(), c.Not(), d.Not()) + fmt.Println("Not Not: ", a.Not().Not(), b.Not().Not(), c.Not().Not(), + d.Not().Not()) + fmt.Print("0.And(.25)→", a.And(b), " .25.And(.75)→", b.And(c), + " .75.And(1)→", c.And(d), " 0.And(.25,.75,1)→", a.And(b, c, d), + "\n") + fmt.Print("0.Or(.25)→", a.Or(b), " .25.Or(.75)→", b.Or(c), + " .75.Or(1)→", c.Or(d), " 0.Or(.25,.75,1)→", a.Or(b, c, d), "\n") + fmt.Println("a < c, a == c, a > c:", a.Less(c), a.Equal(c), c.Less(a)) + fmt.Println("Bool: ", a.Bool(), b.Bool(), c.Bool(), d.Bool()) + fmt.Println("Float: ", a.Float(), b.Float(), c.Float(), d.Float()) +} diff --git a/src/fuzzy_value/fuzzy.go b/src/fuzzy_value/fuzzy.go new file mode 100644 index 0000000..d4006af --- /dev/null +++ b/src/fuzzy_value/fuzzy.go @@ -0,0 +1,47 @@ +// Copyright © 2011-12 Qtrac Ltd. +// +// This program or package and any associated files are licensed under the +// Apache License, Version 2.0 (the "License"); you may not use these files +// except in compliance with the License. You can get a copy of the License +// at: http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package main + +import ( + "fmt" + "fuzzy_value/fuzzybool" +) + +func main() { + var a fuzzybool.FuzzyBool // Same as: a, _ := fuzzybool.New(0) + b, _ := fuzzybool.New(.25) // known valid values; must check if using + c, _ := fuzzybool.New(.75) // variables though. + d := c.Copy() + if err := d.Set(1); err != nil { + fmt.Println(err) + } + process(a, b, c, d) + s := []fuzzybool.FuzzyBool{a, b, c, d} + fmt.Println(s) + m := map[fuzzybool.FuzzyBool]string{a: "a", b: "b", c: "c", c: "d"} + fmt.Println(m) +} + +func process(a, b, c, d fuzzybool.FuzzyBool) { + fmt.Println("Original:", a, b, c, d) + fmt.Println("Not: ", a.Not(), b.Not(), c.Not(), d.Not()) + fmt.Println("Not Not: ", a.Not().Not(), b.Not().Not(), c.Not().Not(), + d.Not().Not()) + fmt.Print("0.And(.25)→", a.And(b), "• .25.And(.75)→", b.And(c), + "• .75.And(1)→", c.And(d), " • .25.And(.75,1)→", b.And(c, d), "\n") + fmt.Print("0.Or(.25)→", a.Or(b), "• .25.Or(.75)→", b.Or(c), + "• .75.Or(1)→", c.Or(d), " • .25.Or(.75,1)→", b.Or(c, d), "\n") + fmt.Println("a < c, a == c, a > c:", a.Less(c), a.Equal(c), c.Less(a)) + fmt.Println("Bool: ", a.Bool(), b.Bool(), c.Bool(), d.Bool()) + fmt.Println("Float: ", a.Float(), b.Float(), c.Float(), d.Float()) +} diff --git a/src/fuzzy_value/fuzzybool/fuzzybool.go b/src/fuzzy_value/fuzzybool/fuzzybool.go new file mode 100644 index 0000000..9e81b79 --- /dev/null +++ b/src/fuzzy_value/fuzzybool/fuzzybool.go @@ -0,0 +1,104 @@ +// Copyright © 2011-12 Qtrac Ltd. +// +// This program or package and any associated files are licensed under the +// Apache License, Version 2.0 (the "License"); you may not use these files +// except in compliance with the License. You can get a copy of the License +// at: http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package fuzzybool + +import "fmt" + +type FuzzyBool struct{ value float32 } + +func New(value interface{}) (FuzzyBool, error) { + amount, err := float32ForValue(value) + return FuzzyBool{amount}, err +} + +func float32ForValue(value interface{}) (fuzzy float32, err error) { + switch value := value.(type) { // shadow variable + case float32: + fuzzy = value + case float64: + fuzzy = float32(value) + case int: + fuzzy = float32(value) + case bool: + fuzzy = 0 + if value { + fuzzy = 1 + } + default: + return 0, fmt.Errorf("float32ForValue(): %v is not a "+ + "number or Boolean", value) + } + if fuzzy < 0 { + fuzzy = 0 + } else if fuzzy > 1 { + fuzzy = 1 + } + return fuzzy, nil +} + +func (fuzzy *FuzzyBool) Set(value interface{}) (err error) { + fuzzy.value, err = float32ForValue(value) + return err +} + +func (fuzzy FuzzyBool) Copy() FuzzyBool { + return FuzzyBool{fuzzy.value} +} + +func (fuzzy FuzzyBool) String() string { + return fmt.Sprintf("%.0f%%", 100*fuzzy.value) +} + +func (fuzzy FuzzyBool) Not() FuzzyBool { + return FuzzyBool{1 - fuzzy.value} +} + +func (fuzzy FuzzyBool) And(first FuzzyBool, + rest ...FuzzyBool) FuzzyBool { + minimum := fuzzy.value + rest = append(rest, first) + for _, other := range rest { + if minimum > other.value { + minimum = other.value + } + } + return FuzzyBool{minimum} +} + +func (fuzzy FuzzyBool) Or(first FuzzyBool, + rest ...FuzzyBool) FuzzyBool { + maximum := fuzzy.value + rest = append(rest, first) + for _, other := range rest { + if maximum < other.value { + maximum = other.value + } + } + return FuzzyBool{maximum} +} + +func (fuzzy FuzzyBool) Less(other FuzzyBool) bool { + return fuzzy.value < other.value +} + +func (fuzzy FuzzyBool) Equal(other FuzzyBool) bool { + return fuzzy.value == other.value +} + +func (fuzzy FuzzyBool) Bool() bool { + return fuzzy.value >= .5 +} + +func (fuzzy FuzzyBool) Float() float64 { + return float64(fuzzy.value) +} diff --git a/src/guess_separator/guess_separator.go b/src/guess_separator/guess_separator.go new file mode 100644 index 0000000..5862bda --- /dev/null +++ b/src/guess_separator/guess_separator.go @@ -0,0 +1,103 @@ +// Copyright © 2011-12 Qtrac Ltd. +// +// This program or package and any associated files are licensed under the +// Apache License, Version 2.0 (the "License"); you may not use these files +// except in compliance with the License. You can get a copy of the License +// at: http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "bufio" + "fmt" + "io" + "log" + "os" + "path/filepath" + "strings" +) + +func main() { + if len(os.Args) == 1 || os.Args[1] == "-h" || os.Args[1] == "--help" { + fmt.Printf("usage: %s file\n", filepath.Base(os.Args[0])) + os.Exit(1) + } + + separators := []string{"\t", "*", "|", "•"} + + linesRead, lines := readUpToNLines(os.Args[1], 5) + counts := createCounts(lines, separators, linesRead) + separator := guessSep(counts, separators, linesRead) + report(separator) +} + +func createCounts(lines, separators []string, linesRead int) [][]int { + counts := make([][]int, len(separators)) + for sepIndex := range separators { + counts[sepIndex] = make([]int, linesRead) + for lineIndex, line := range lines { + counts[sepIndex][lineIndex] = + strings.Count(line, separators[sepIndex]) + } + } + return counts +} + +func guessSep(counts [][]int, separators []string, linesRead int) string { + for sepIndex := range separators { + same := true + count := counts[sepIndex][0] + for lineIndex := 1; lineIndex < linesRead; lineIndex++ { + if counts[sepIndex][lineIndex] != count { + same = false + break + } + } + if count > 0 && same { + return separators[sepIndex] + } + } + return "" +} + +func report(separator string) { + switch separator { + case "": + fmt.Println("whitespace-separated or not separated at all") + case "\t": + fmt.Println("tab-separated") + default: + fmt.Printf("%s-separated\n", separator) + } +} + +func readUpToNLines(filename string, maxLines int) (int, []string) { + var file *os.File + var err error + if file, err = os.Open(filename); err != nil { + log.Fatal("failed to open the file: ", err) + } + defer file.Close() + lines := make([]string, maxLines) + reader := bufio.NewReader(file) + i := 0 + for ; i < maxLines; i++ { + line, err := reader.ReadString('\n') + if line != "" { + lines[i] = line + } + if err != nil { + if err == io.EOF { + break + } + log.Fatal("failed to finish reading the file: ", err) + } + } // Return the subslice actually used; could be < maxLines + return i, lines[:i] +} diff --git a/src/hello/hello.go b/src/hello/hello.go new file mode 100644 index 0000000..e8f51d2 --- /dev/null +++ b/src/hello/hello.go @@ -0,0 +1,29 @@ +// Copyright © 2010-12 Qtrac Ltd. +// +// This program or package and any associated files are licensed under the +// Apache License, Version 2.0 (the "License"); you may not use these files +// except in compliance with the License. You can get a copy of the License +// at: http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// hello.go +package main + +import ( + "fmt" + "os" + "strings" +) + +func main() { + who := "World!" + if len(os.Args) > 1 { /* os.Args[0] is "hello" or "hello.exe" */ + who = strings.Join(os.Args[1:], " ") + } + fmt.Println("Hello", who) +} diff --git a/src/imagetag1/imagetag1.go b/src/imagetag1/imagetag1.go new file mode 100644 index 0000000..4ae8f5e --- /dev/null +++ b/src/imagetag1/imagetag1.go @@ -0,0 +1,74 @@ +// Copyright © 2011-12 Qtrac Ltd. +// +// This program or package and any associated files are licensed under the +// Apache License, Version 2.0 (the "License"); you may not use these files +// except in compliance with the License. You can get a copy of the License +// at: http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "fmt" + "image" + "os" + "path/filepath" + "runtime" + + _ "image/gif" + _ "image/jpeg" + _ "image/png" +) + +func main() { + runtime.GOMAXPROCS(runtime.NumCPU()) // Use all the machine's cores + if len(os.Args) == 1 || os.Args[1] == "-h" || os.Args[1] == "--help" { + fmt.Printf("usage: %s \n", + filepath.Base(os.Args[0])) + os.Exit(1) + } + + files := commandLineFiles(os.Args[1:]) + for _, filename := range files { + process(filename) + } +} + +func process(filename string) { + if info, err := os.Stat(filename); err != nil || + (info.Mode()&os.ModeType != 0) { + return // Ignore errors and nonregular files + } + file, err := os.Open(filename) + if err != nil { + return // Ignore errors + } + defer file.Close() + config, _, err := image.DecodeConfig(file) + if err != nil { + return // Ignore errors + } + fmt.Printf(``, + filepath.Base(filename), config.Width, config.Height) + fmt.Println() +} + +func commandLineFiles(files []string) []string { + if runtime.GOOS == "windows" { + args := make([]string, 0, len(files)) + for _, name := range files { + if matches, err := filepath.Glob(name); err != nil { + args = append(args, name) // Invalid pattern + } else if matches != nil { // At least one match + args = append(args, matches...) + } + } + return args + } + return files +} diff --git a/src/imagetag2/imagetag2.go b/src/imagetag2/imagetag2.go new file mode 100644 index 0000000..9972ce4 --- /dev/null +++ b/src/imagetag2/imagetag2.go @@ -0,0 +1,118 @@ +// Copyright © 2011-12 Qtrac Ltd. +// +// This program or package and any associated files are licensed under the +// Apache License, Version 2.0 (the "License"); you may not use these files +// except in compliance with the License. You can get a copy of the License +// at: http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "fmt" + "image" + "os" + "path/filepath" + "runtime" + + _ "image/gif" + _ "image/jpeg" + _ "image/png" +) + +var workers = runtime.NumCPU() + +func main() { + runtime.GOMAXPROCS(runtime.NumCPU()) // Use all the machine's cores + if len(os.Args) == 1 || os.Args[1] == "-h" || os.Args[1] == "--help" { + fmt.Printf("usage: %s \n", + filepath.Base(os.Args[0])) + os.Exit(1) + } + + files := commandLineFiles(os.Args[1:]) + jobs := make(chan string, workers*16) + results := make(chan string) + done := make(chan struct{}, workers) + + go addJobs(files, jobs) + for i := 0; i < workers; i++ { + go doJobs(done, results, jobs) + } + waitAndProcessResults(done, results) +} + +func addJobs(files []string, jobs chan<- string) { + for _, filename := range files { + jobs <- filename + } + close(jobs) +} + +func doJobs(done chan<- struct{}, results chan<- string, + jobs <-chan string) { + for job := range jobs { + if result, ok := process(job); ok { + results <- result + } + } + done <- struct{}{} +} + +func waitAndProcessResults(done <-chan struct{}, results <-chan string) { + for working := workers; working > 0; { + select { // Blocking + case result := <-results: + fmt.Println(result) + case <-done: + working-- + } + } +DONE: + for { + select { // Nonblocking + case result := <-results: + fmt.Println(result) + default: + break DONE + } + } +} + +func process(filename string) (string, bool) { + if info, err := os.Stat(filename); err != nil || + (info.Mode()&os.ModeType == 1) { + return "", false // Ignore errors and nonregular files + } + file, err := os.Open(filename) + if err != nil { + return "", false // Ignore errors + } + defer file.Close() + config, _, err := image.DecodeConfig(file) + if err != nil { + return "", false // Ignore errors + } + return fmt.Sprintf(``, + filepath.Base(filename), config.Width, config.Height), true +} + +func commandLineFiles(files []string) []string { + if runtime.GOOS == "windows" { + args := make([]string, 0, len(files)) + for _, name := range files { + if matches, err := filepath.Glob(name); err != nil { + args = append(args, name) // Invalid pattern + } else if matches != nil { // At least one match + args = append(args, matches...) + } + } + return args + } + return files +} diff --git a/src/indent_sort/indent_sort.go b/src/indent_sort/indent_sort.go new file mode 100644 index 0000000..c5ac8e6 --- /dev/null +++ b/src/indent_sort/indent_sort.go @@ -0,0 +1,139 @@ +// Copyright © 2011-12 Qtrac Ltd. +// +// This program or package and any associated files are licensed under the +// Apache License, Version 2.0 (the "License"); you may not use these files +// except in compliance with the License. You can get a copy of the License +// at: http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "fmt" + "sort" + "strings" +) + +var original = []string{ + "Nonmetals", + " Hydrogen", + " Carbon", + " Nitrogen", + " Oxygen", + "Inner Transitionals", + " Lanthanides", + " Europium", + " Cerium", + " Actinides", + " Uranium", + " Plutonium", + " Curium", + "Alkali Metals", + " Lithium", + " Sodium", + " Potassium", +} + +func main() { + fmt.Println("| Original | Sorted |") + fmt.Println("|-------------------|-------------------|") + sorted := SortedIndentedStrings(original) // original is a []string + for i := range original { // set in a global var + fmt.Printf("|%-19s|%-19s|\n", original[i], sorted[i]) + } +} + +/* + Given a []string that has items with different levels of indent that + are used to indicate parent → child relationships, sorts the items + case-insensitively with child items sorted underneath their parent + items, and so on recursively to any level of depth. + The amount of indent per level is computed by finding the first + indented item. Indentation must either be one or more spaces or one or + more tabs. +*/ +func SortedIndentedStrings(slice []string) []string { + entries := populateEntries(slice) + return sortedEntries(entries) +} + +func populateEntries(slice []string) Entries { + indent, indentSize := computeIndent(slice) + entries := make(Entries, 0) + for _, item := range slice { + i, level := 0, 0 + for strings.HasPrefix(item[i:], indent) { + i += indentSize + level++ + } + key := strings.ToLower(strings.TrimSpace(item)) + addEntry(level, key, item, &entries) + } + return entries +} + +func computeIndent(slice []string) (string, int) { + for _, item := range slice { + if len(item) > 0 && (item[0] == ' ' || item[0] == '\t') { + whitespace := rune(item[0]) + for i, char := range item[1:] { + if char != whitespace { + return strings.Repeat(string(whitespace), i), i + } + } + } + } + return "", 0 +} + +func addEntry(level int, key, value string, entries *Entries) { + if level == 0 { + *entries = append(*entries, Entry{key, value, make(Entries, 0)}) + } else { + /* + theEntries := *entries + lastEntry := &theEntries[theEntries.Len()-1] + addEntry(level-1, key, value, &lastEntry.children) + */ + addEntry(level-1, key, value, + &((*entries)[entries.Len()-1].children)) + } +} + +func sortedEntries(entries Entries) []string { + var indentedSlice []string + sort.Sort(entries) + for _, entry := range entries { + populateIndentedStrings(entry, &indentedSlice) + } + return indentedSlice +} + +func populateIndentedStrings(entry Entry, indentedSlice *[]string) { + *indentedSlice = append(*indentedSlice, entry.value) + sort.Sort(entry.children) + for _, child := range entry.children { + populateIndentedStrings(child, indentedSlice) + } +} + +type Entry struct { + key string + value string + children Entries +} +type Entries []Entry + +func (entries Entries) Len() int { return len(entries) } + +func (entries Entries) Less(i, j int) bool { + return entries[i].key < entries[j].key +} +func (entries Entries) Swap(i, j int) { + entries[i], entries[j] = entries[j], entries[i] +} diff --git a/src/invoicedata/gob.go b/src/invoicedata/gob.go new file mode 100644 index 0000000..c118d65 --- /dev/null +++ b/src/invoicedata/gob.go @@ -0,0 +1,96 @@ +// Copyright © 2011-12 Qtrac Ltd. +// +// This program or package and any associated files are licensed under the +// Apache License, Version 2.0 (the "License"); you may not use these files +// except in compliance with the License. You can get a copy of the License +// at: http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( +// "bytes" + "encoding/gob" + "errors" + "fmt" + "io" +// "time" +) +/* + // Here is how to make a custom type satisfy the gob.Encoder and + // gob.Decoder interfaces. + +type GobInvoice struct { + Id int + CustomerId int + Raised int64 // Seconds since the Unix epoch + Due int64 // Seconds since the Unix epoch + Paid bool + Note string + Items []*Item +} + +func (invoice *Invoice) GobEncode() ([]byte, error) { + gobInvoice := GobInvoice{invoice.Id, invoice.CustomerId, + invoice.Raised.Unix(), invoice.Due.Unix(), invoice.Paid, + invoice.Note, invoice.Items} + var buffer bytes.Buffer + encoder := gob.NewEncoder(&buffer) + err := encoder.Encode(gobInvoice) + return buffer.Bytes(), err +} + +func (invoice *Invoice) GobDecode(data []byte) error { + var gobInvoice GobInvoice + buffer := bytes.NewBuffer(data) + decoder := gob.NewDecoder(buffer) + if err := decoder.Decode(&gobInvoice); err != nil { + return err + } + raised := time.Unix(gobInvoice.Raised, 0) + due := time.Unix(gobInvoice.Due, 0) + *invoice = Invoice{gobInvoice.Id, gobInvoice.CustomerId, raised, due, + gobInvoice.Paid, gobInvoice.Note, gobInvoice.Items} + return nil +} +*/ +type GobMarshaler struct{} + +func (GobMarshaler) MarshalInvoices(writer io.Writer, + invoices []*Invoice) error { + encoder := gob.NewEncoder(writer) + if err := encoder.Encode(magicNumber); err != nil { + return err + } + if err := encoder.Encode(fileVersion); err != nil { + return err + } + return encoder.Encode(invoices) +} + +func (GobMarshaler) UnmarshalInvoices(reader io.Reader) ([]*Invoice, + error) { + decoder := gob.NewDecoder(reader) + var magic int + if err := decoder.Decode(&magic); err != nil { + return nil, err + } + if magic != magicNumber { + return nil, errors.New("cannot read non-invoices gob file") + } + var version int + if err := decoder.Decode(&version); err != nil { + return nil, err + } + if version > fileVersion { + return nil, fmt.Errorf("version %d is too new to read", version) + } + var invoices []*Invoice + err := decoder.Decode(&invoices) + return invoices, err +} diff --git a/src/invoicedata/inv.go b/src/invoicedata/inv.go new file mode 100644 index 0000000..f715767 --- /dev/null +++ b/src/invoicedata/inv.go @@ -0,0 +1,246 @@ +// Copyright © 2011-12 Qtrac Ltd. +// +// This program or package and any associated files are licensed under the +// Apache License, Version 2.0 (the "License"); you may not use these files +// except in compliance with the License. You can get a copy of the License +// at: http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "encoding/binary" + "errors" + "fmt" + "io" + "strconv" + "time" +) + +type InvMarshaler struct{} + +const invDateFormat = "20060102" // This date must always be used. + +var byteOrder = binary.LittleEndian + +func (InvMarshaler) MarshalInvoices(writer io.Writer, + invoices []*Invoice) error { + var write invWriterFunc = func(x interface{}) error { + return binary.Write(writer, byteOrder, x) + } + if err := write(uint32(magicNumber)); err != nil { + return err + } + if err := write(uint16(fileVersion)); err != nil { + return err + } + if err := write(int32(len(invoices))); err != nil { + return err + } + for _, invoice := range invoices { + if err := write.writeInvoice(invoice); err != nil { + return err + } + } + return nil +} + +type invWriterFunc func(interface{}) error + +func (write invWriterFunc) writeInvoice(invoice *Invoice) error { + for _, i := range []int{invoice.Id, invoice.CustomerId} { + if err := write(int32(i)); err != nil { + return err + } + } + for _, date := range []time.Time{invoice.Raised, invoice.Due} { + if err := write.writeDate(date); err != nil { + return err + } + } + if err := write.writeBool(invoice.Paid); err != nil { + return err + } + if err := write.writeString(invoice.Note); err != nil { + return err + } + if err := write(int32(len(invoice.Items))); err != nil { + return err + } + for _, item := range invoice.Items { + if err := write.writeItem(item); err != nil { + return err + } + } + return nil +} + +func (write invWriterFunc) writeDate(date time.Time) error { + i, err := strconv.Atoi(date.Format(invDateFormat)) + if err != nil { + return err + } + return write(int32(i)) +} + +func (write invWriterFunc) writeBool(b bool) error { + var v int8 + if b { + v = 1 + } + return write(v) +} + +func (write invWriterFunc) writeString(s string) error { + if err := write(int32(len(s))); err != nil { + return err + } + return write([]byte(s)) +} + +func (write invWriterFunc) writeItem(item *Item) error { + if err := write.writeString(item.Id); err != nil { + return err + } + if err := write(item.Price); err != nil { + return err + } + if err := write(int16(item.Quantity)); err != nil { + return err + } + return write.writeString(item.Note) +} + +func (InvMarshaler) UnmarshalInvoices(reader io.Reader) ([]*Invoice, + error) { + if err := checkInvVersion(reader); err != nil { + return nil, err + } + count, err := readIntFromInt32(reader) + if err != nil { + return nil, err + } + invoices := make([]*Invoice, 0, count) + for i := 0; i < count; i++ { + invoice, err := readInvInvoice(reader) + if err != nil { + return nil, err + } + invoices = append(invoices, invoice) + } + return invoices, nil +} + +func readIntFromInt32(reader io.Reader) (int, error) { + var i32 int32 + err := binary.Read(reader, byteOrder, &i32) + return int(i32), err +} + +func readIntFromInt16(reader io.Reader) (int, error) { + var i16 int16 + err := binary.Read(reader, byteOrder, &i16) + return int(i16), err +} + +func readBoolFromInt8(reader io.Reader) (bool, error) { + var i8 int8 + err := binary.Read(reader, byteOrder, &i8) + return i8 == 1, err +} + +func checkInvVersion(reader io.Reader) error { + var magic uint32 + if err := binary.Read(reader, byteOrder, &magic); err != nil { + return err + } + if magic != magicNumber { + return errors.New("cannot read non-invoices inv file") + } + var version uint16 + if err := binary.Read(reader, byteOrder, &version); err != nil { + return err + } + if version > fileVersion { + return fmt.Errorf("version %d is too new to read", version) + } + return nil +} + +func readInvInvoice(reader io.Reader) (invoice *Invoice, err error) { + invoice = &Invoice{} + for _, pId := range []*int{&invoice.Id, &invoice.CustomerId} { + if *pId, err = readIntFromInt32(reader); err != nil { + return nil, err + } + } + for _, pDate := range []*time.Time{&invoice.Raised, &invoice.Due} { + if *pDate, err = readInvDate(reader); err != nil { + return nil, err + } + } + if invoice.Paid, err = readBoolFromInt8(reader); err != nil { + return nil, err + } + if invoice.Note, err = readInvString(reader); err != nil { + return nil, err + } + var count int + if count, err = readIntFromInt32(reader); err != nil { + return nil, err + } + invoice.Items, err = readInvItems(reader, count) + return invoice, err +} + +func readInvItems(reader io.Reader, count int) ([]*Item, error) { + items := make([]*Item, 0, count) + for i := 0; i < count; i++ { + item, err := readInvItem(reader) + if err != nil { + return nil, err + } + items = append(items, item) + } + return items, nil +} + +func readInvDate(reader io.Reader) (time.Time, error) { + var n int32 + if err := binary.Read(reader, byteOrder, &n); err != nil { + return time.Time{}, err + } + return time.Parse(invDateFormat, fmt.Sprint(n)) +} + +func readInvString(reader io.Reader) (string, error) { + var length int32 + if err := binary.Read(reader, byteOrder, &length); err != nil { + return "", nil + } + raw := make([]byte, length) + if err := binary.Read(reader, byteOrder, &raw); err != nil { + return "", err + } + return string(raw), nil +} + +func readInvItem(reader io.Reader) (item *Item, err error) { + item = &Item{} + if item.Id, err = readInvString(reader); err != nil { + return nil, err + } + if err = binary.Read(reader, byteOrder, &item.Price); err != nil { + return nil, err + } + if item.Quantity, err = readIntFromInt16(reader); err != nil { + return nil, err + } + item.Note, err = readInvString(reader) + return item, nil +} diff --git a/src/invoicedata/invoicedata.go b/src/invoicedata/invoicedata.go new file mode 100644 index 0000000..a8d90c8 --- /dev/null +++ b/src/invoicedata/invoicedata.go @@ -0,0 +1,205 @@ +// Copyright © 2011-12 Qtrac Ltd. +// +// This program or package and any associated files are licensed under the +// Apache License, Version 2.0 (the "License"); you may not use these files +// except in compliance with the License. You can get a copy of the License +// at: http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "compress/gzip" + "errors" + "fmt" + "io" + "log" + "os" + "path/filepath" + "strings" + "time" +) + +const ( + fileType = "INVOICES" // Used by text formats + magicNumber = 0x125D // Used by binary formats + fileVersion = 100 // Used by all formats + dateFormat = "2006-01-02" // This date must always be used + nanosecondsToSeconds = 1e9 +) + +type Invoice struct { + Id int + CustomerId int + Raised time.Time + Due time.Time + Paid bool + Note string + Items []*Item +} + +type Item struct { + Id string + Price float64 + Quantity int + Note string +} + +type InvoicesMarshaler interface { + MarshalInvoices(writer io.Writer, invoices []*Invoice) error +} + +type InvoicesUnmarshaler interface { + UnmarshalInvoices(reader io.Reader) ([]*Invoice, error) +} + +func main() { + log.SetFlags(0) + report := false + args := os.Args[1:] + if len(args) > 0 && (args[0] == "-t" || args[0] == "--time") { + report = true + args = args[1:] + } + if len(args) != 2 || args[0] == "-h" || args[0] == "--help" { + log.Fatalf("usage: %s [-t|--time] infile.ext outfile.ext\n"+ + ".ext may be any of .gob, .inv, .jsn, .json, .txt, "+ + "or .xml, optionally gzipped (e.g., .gob.gz)\n", + filepath.Base(os.Args[0])) + } + inFilename, outFilename := args[0], args[1] + if inFilename == outFilename { + log.Fatalln("won't overwrite a file with itself") + } + + start := time.Now() + invoices, err := readInvoiceFile(inFilename) + if err == nil && report { + duration := time.Now().Sub(start) + fmt.Printf("Read %s in %.3f seconds\n", inFilename, + float64(duration)/nanosecondsToSeconds) + } + if err != nil { + log.Fatalln("Failed to read:", err) + } + start = time.Now() + err = writeInvoiceFile(outFilename, invoices) + if err == nil && report { + duration := time.Now().Sub(start) + fmt.Printf("Wrote %s in %.3f seconds\n", outFilename, + float64(duration)/nanosecondsToSeconds) + } + if err != nil { + log.Fatalln("Failed to write:", err) + } +} + +func readInvoiceFile(filename string) ([]*Invoice, error) { + file, closer, err := openInvoiceFile(filename) + if closer != nil { + defer closer() + } + if err != nil { + return nil, err + } + return readInvoices(file, suffixOf(filename)) +} + +func openInvoiceFile(filename string) (io.ReadCloser, func(), error) { + file, err := os.Open(filename) + if err != nil { + return nil, nil, err + } + closer := func() { file.Close() } + var reader io.ReadCloser = file + var decompressor *gzip.Reader + if strings.HasSuffix(filename, ".gz") { + if decompressor, err = gzip.NewReader(file); err != nil { + return file, closer, err + } + closer = func() { decompressor.Close(); file.Close() } + reader = decompressor + } + return reader, closer, nil +} + +func readInvoices(reader io.Reader, suffix string) ([]*Invoice, error) { + var unmarshaler InvoicesUnmarshaler + switch suffix { + case ".gob": + unmarshaler = GobMarshaler{} + case ".inv": + unmarshaler = InvMarshaler{} + case ".jsn", ".json": + unmarshaler = JSONMarshaler{} + case ".txt": + unmarshaler = TxtMarshaler{} + case ".xml": + unmarshaler = XMLMarshaler{} + } + if unmarshaler != nil { + return unmarshaler.UnmarshalInvoices(reader) + } + return nil, fmt.Errorf("unrecognized input suffix: %s", suffix) +} + +func writeInvoiceFile(filename string, invoices []*Invoice) error { + file, closer, err := createInvoiceFile(filename) + if closer != nil { + defer closer() + } + if err != nil { + return err + } + return writeInvoices(file, suffixOf(filename), invoices) +} + +func createInvoiceFile(filename string) (io.WriteCloser, func(), error) { + file, err := os.Create(filename) + if err != nil { + return nil, nil, err + } + closer := func() { file.Close() } + var writer io.WriteCloser = file + var compressor *gzip.Writer + if strings.HasSuffix(filename, ".gz") { + compressor = gzip.NewWriter(file) + closer = func() { compressor.Close(); file.Close() } + writer = compressor + } + return writer, closer, nil +} + +func writeInvoices(writer io.Writer, suffix string, + invoices []*Invoice) error { + var marshaler InvoicesMarshaler + switch suffix { + case ".gob": + marshaler = GobMarshaler{} + case ".inv": + marshaler = InvMarshaler{} + case ".jsn", ".json": + marshaler = JSONMarshaler{} + case ".txt": + marshaler = TxtMarshaler{} + case ".xml": + marshaler = XMLMarshaler{} + } + if marshaler != nil { + return marshaler.MarshalInvoices(writer, invoices) + } + return errors.New("unrecognized output suffix") +} + +func suffixOf(filename string) string { + suffix := filepath.Ext(filename) + if suffix == ".gz" { + suffix = filepath.Ext(filename[:len(filename)-3]) + } + return suffix +} diff --git a/src/invoicedata/invoices.gob.gz b/src/invoicedata/invoices.gob.gz new file mode 100644 index 0000000..b0bc28c Binary files /dev/null and b/src/invoicedata/invoices.gob.gz differ diff --git a/src/invoicedata/jsn.go b/src/invoicedata/jsn.go new file mode 100644 index 0000000..3386fab --- /dev/null +++ b/src/invoicedata/jsn.go @@ -0,0 +1,106 @@ +// Copyright © 2011-12 Qtrac Ltd. +// +// This program or package and any associated files are licensed under the +// Apache License, Version 2.0 (the "License"); you may not use these files +// except in compliance with the License. You can get a copy of the License +// at: http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "encoding/json" + "errors" + "fmt" + "io" + "time" +) + +type JSONInvoice struct { + Id int + CustomerId int + Raised string // time.Time in Invoice struct + Due string // time.Time in Invoice struct + Paid bool + Note string + Items []*Item +} + +func (invoice Invoice) MarshalJSON() ([]byte, error) { + jsonInvoice := JSONInvoice{ + invoice.Id, + invoice.CustomerId, + invoice.Raised.Format(dateFormat), + invoice.Due.Format(dateFormat), + invoice.Paid, + invoice.Note, + invoice.Items, + } + return json.Marshal(jsonInvoice) +} + +func (invoice *Invoice) UnmarshalJSON(data []byte) (err error) { + var jsonInvoice JSONInvoice + if err = json.Unmarshal(data, &jsonInvoice); err != nil { + return err + } + var raised, due time.Time + if raised, err = time.Parse(dateFormat, jsonInvoice.Raised); + err != nil { + return err + } + if due, err = time.Parse(dateFormat, jsonInvoice.Due); err != nil { + return err + } + *invoice = Invoice{ + jsonInvoice.Id, + jsonInvoice.CustomerId, + raised, + due, + jsonInvoice.Paid, + jsonInvoice.Note, + jsonInvoice.Items, + } + return nil +} + +type JSONMarshaler struct{} + +func (JSONMarshaler) MarshalInvoices(writer io.Writer, + invoices []*Invoice) error { + encoder := json.NewEncoder(writer) + if err := encoder.Encode(fileType); err != nil { + return err + } + if err := encoder.Encode(fileVersion); err != nil { + return err + } + return encoder.Encode(invoices) +} + +func (JSONMarshaler) UnmarshalInvoices(reader io.Reader) ([]*Invoice, + error) { + decoder := json.NewDecoder(reader) + var kind string + if err := decoder.Decode(&kind); err != nil { + return nil, err + } + if kind != fileType { + return nil, errors.New("cannot read non-invoices json file") + } + var version int + if err := decoder.Decode(&version); err != nil { + return nil, err + } + if version > fileVersion { + return nil, fmt.Errorf("version %d is too new to read", version) + } + var invoices []*Invoice + err := decoder.Decode(&invoices) + return invoices, err +} diff --git a/src/invoicedata/txt.go b/src/invoicedata/txt.go new file mode 100644 index 0000000..70df329 --- /dev/null +++ b/src/invoicedata/txt.go @@ -0,0 +1,167 @@ +// Copyright © 2011-12 Qtrac Ltd. +// +// This program or package and any associated files are licensed under the +// Apache License, Version 2.0 (the "License"); you may not use these files +// except in compliance with the License. You can get a copy of the License +// at: http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "bufio" + "errors" + "fmt" + "io" + "strings" + "time" +) + +const noteSep = ":" + +type TxtMarshaler struct{} + +func (TxtMarshaler) MarshalInvoices(writer io.Writer, + invoices []*Invoice) error { + bufferedWriter := bufio.NewWriter(writer) + defer bufferedWriter.Flush() + var write writerFunc = func(format string, + args ...interface{}) error { + _, err := fmt.Fprintf(bufferedWriter, format, args...) + return err + } + if err := write("%s %d\n", fileType, fileVersion); err != nil { + return err + } + for _, invoice := range invoices { + if err := write.writeInvoice(invoice); err != nil { + return err + } + } + return nil +} + +type writerFunc func(string, ...interface{}) error + +func (write writerFunc) writeInvoice(invoice *Invoice) error { + note := "" + if invoice.Note != "" { + note = noteSep + " " + invoice.Note + } + if err := write("INVOICE ID=%d CUSTOMER=%d RAISED=%s DUE=%s "+ + "PAID=%t%s\n", invoice.Id, invoice.CustomerId, + invoice.Raised.Format(dateFormat), + invoice.Due.Format(dateFormat), invoice.Paid, note); err != nil { + return err + } + if err := write.writeItems(invoice.Items); err != nil { + return err + } + return write("\f\n") +} + +func (write writerFunc) writeItems(items []*Item) error { + for _, item := range items { + note := "" + if item.Note != "" { + note = noteSep + " " + item.Note + } + if err := write("ITEM ID=%s PRICE=%.2f QUANTITY=%d%s\n", item.Id, + item.Price, item.Quantity, note); err != nil { + return err + } + } + return nil +} + +func (TxtMarshaler) UnmarshalInvoices(reader io.Reader) ([]*Invoice, + error) { + bufferedReader := bufio.NewReader(reader) + if err := checkTxtVersion(bufferedReader); err != nil { + return nil, err + } + var invoices []*Invoice + eof := false + for lino := 2; !eof; lino++ { + line, err := bufferedReader.ReadString('\n') + if err == io.EOF { + err = nil // io.EOF isn't really an error + eof = true // this will end the loop at the next iteration + } else if err != nil { + return nil, err // finish immediately for real errors + } + if invoices, err = parseTxtLine(lino, line, invoices); err != nil { + return nil, err + } + } + return invoices, nil +} + +func checkTxtVersion(bufferedReader *bufio.Reader) error { + var version int + if _, err := fmt.Fscanf(bufferedReader, "INVOICES %d\n", &version); + err != nil { + return errors.New("cannot read non-invoices text file") + } else if version > fileVersion { + return fmt.Errorf("version %d is too new to read", version) + } + return nil +} + +func parseTxtLine(lino int, line string, invoices []*Invoice) ([]*Invoice, + error) { + var err error + if strings.HasPrefix(line, "INVOICE") { + var invoice *Invoice + invoice, err = parseTxtInvoice(lino, line) + invoices = append(invoices, invoice) + } else if strings.HasPrefix(line, "ITEM") { + if len(invoices) == 0 { + err = fmt.Errorf("item outside of an invoice line %d", lino) + } else { + var item *Item + item, err = parseTxtItem(lino, line) + items := &invoices[len(invoices)-1].Items + *items = append(*items, item) + } + } + return invoices, err +} + +func parseTxtInvoice(lino int, line string) (invoice *Invoice, + err error) { + invoice = &Invoice{} + var raised, due string + if _, err = fmt.Sscanf(line, "INVOICE ID=%d CUSTOMER=%d "+ + "RAISED=%s DUE=%s PAID=%t", &invoice.Id, &invoice.CustomerId, + &raised, &due, &invoice.Paid); err != nil { + return nil, fmt.Errorf("invalid invoice %v line %d", err, lino) + } + if invoice.Raised, err = time.Parse(dateFormat, raised); err != nil { + return nil, fmt.Errorf("invalid raised %v line %d", err, lino) + } + if invoice.Due, err = time.Parse(dateFormat, due); err != nil { + return nil, fmt.Errorf("invalid due %v line %d", err, lino) + } + if i := strings.Index(line, noteSep); i > -1 { + invoice.Note = strings.TrimSpace(line[i+len(noteSep):]) + } + return invoice, nil +} + +func parseTxtItem(lino int, line string) (item *Item, err error) { + item = &Item{} + if _, err = fmt.Sscanf(line, "ITEM ID=%s PRICE=%f QUANTITY=%d", + &item.Id, &item.Price, &item.Quantity); err != nil { + return nil, fmt.Errorf("invalid item %v line %d", err, lino) + } + if i := strings.Index(line, noteSep); i > -1 { + item.Note = strings.TrimSpace(line[i+len(noteSep):]) + } + return item, nil +} diff --git a/src/invoicedata/xml.go b/src/invoicedata/xml.go new file mode 100644 index 0000000..58e95cf --- /dev/null +++ b/src/invoicedata/xml.go @@ -0,0 +1,154 @@ +// Copyright © 2011-12 Qtrac Ltd. +// +// This program or package and any associated files are licensed under the +// Apache License, Version 2.0 (the "License"); you may not use these files +// except in compliance with the License. You can get a copy of the License +// at: http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "encoding/xml" + "fmt" + "io" + "strings" + "time" +) + +type XMLMarshaler struct{} + +type XMLInvoices struct { + XMLName xml.Name `xml:"INVOICES"` + Version int `xml:"version,attr"` + Invoice []*XMLInvoice `xml:"INVOICE"` +} + +type XMLInvoice struct { + XMLName xml.Name `xml:"INVOICE"` + Id int `xml:",attr"` + CustomerId int `xml:",attr"` + Raised string `xml:",attr"` + Due string `xml:",attr"` + Paid bool `xml:",attr"` + Note string `xml:"NOTE"` + Item []*XMLItem `xml:"ITEM"` +} + +type XMLItem struct { + XMLName xml.Name `xml:"ITEM"` + Id string `xml:",attr"` + Price float64 `xml:",attr"` + Quantity int `xml:",attr"` + Note string `xml:"NOTE"` +} + + +func XMLInvoicesForInvoices(invoices []*Invoice) *XMLInvoices { + xmlInvoices := &XMLInvoices{ + Version: fileVersion, + Invoice: make([]*XMLInvoice, 0, len(invoices)), + } + for _, invoice := range invoices { + xmlInvoices.Invoice = append(xmlInvoices.Invoice, + XMLInvoiceForInvoice(invoice)) + } + return xmlInvoices +} + + +func XMLInvoiceForInvoice(invoice *Invoice) *XMLInvoice { + xmlInvoice := &XMLInvoice{ + Id: invoice.Id, + CustomerId: invoice.CustomerId, + Raised: invoice.Raised.Format(dateFormat), + Due: invoice.Due.Format(dateFormat), + Paid: invoice.Paid, + Note: invoice.Note, + Item: make([]*XMLItem, 0, len(invoice.Items)), + } + for _, item := range invoice.Items { + xmlItem := &XMLItem{ + Id: item.Id, + Price: item.Price, + Quantity: item.Quantity, + Note: item.Note, + } + xmlInvoice.Item = append(xmlInvoice.Item, xmlItem) + } + return xmlInvoice +} + + +func (xmlInvoices *XMLInvoices) Invoices() (invoices []*Invoice, + err error) { + invoices = make([]*Invoice, 0, len(xmlInvoices.Invoice)) + for _, xmlInvoice := range xmlInvoices.Invoice { + invoice, err := xmlInvoice.Invoice() + if err != nil { + return nil, err + } + invoices = append(invoices, invoice) + } + return invoices, nil +} + + +func (xmlInvoice *XMLInvoice) Invoice() (invoice *Invoice, err error) { + invoice = &Invoice{ + Id: xmlInvoice.Id, + CustomerId: xmlInvoice.CustomerId, + Paid: xmlInvoice.Paid, + Note: strings.TrimSpace(xmlInvoice.Note), + Items: make([]*Item, 0, len(xmlInvoice.Item)), + } + if invoice.Raised, err = time.Parse(dateFormat, xmlInvoice.Raised); + err != nil { + return nil, err + } + if invoice.Due, err = time.Parse(dateFormat, xmlInvoice.Due); + err != nil { + return nil, err + } + for _, xmlItem := range xmlInvoice.Item { + item := &Item{ + Id: xmlItem.Id, + Price: xmlItem.Price, + Quantity: xmlItem.Quantity, + Note: strings.TrimSpace(xmlItem.Note), + } + invoice.Items = append(invoice.Items, item) + } + return invoice, nil +} + + +func (XMLMarshaler) MarshalInvoices(writer io.Writer, + invoices []*Invoice) error { + if _, err := writer.Write([]byte(xml.Header)); err != nil { + return err + } + xmlInvoices := XMLInvoicesForInvoices(invoices) + encoder := xml.NewEncoder(writer) + return encoder.Encode(xmlInvoices) +} + + +func (XMLMarshaler) UnmarshalInvoices(reader io.Reader) ([]*Invoice, + error) { + xmlInvoices := &XMLInvoices{} + decoder := xml.NewDecoder(reader) + if err := decoder.Decode(xmlInvoices); err != nil { + return nil, err + } + if xmlInvoices.Version > fileVersion { + return nil, fmt.Errorf("version %d is too new to read", + xmlInvoices.Version) + } + return xmlInvoices.Invoices() +} diff --git a/src/invoicedata_ans/gob.go b/src/invoicedata_ans/gob.go new file mode 100644 index 0000000..42ef0c9 --- /dev/null +++ b/src/invoicedata_ans/gob.go @@ -0,0 +1,116 @@ +// Copyright © 2011-12 Qtrac Ltd. +// +// This program or package and any associated files are licensed under the +// Apache License, Version 2.0 (the "License"); you may not use these files +// except in compliance with the License. You can get a copy of the License +// at: http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( +// "bytes" + "encoding/gob" + "errors" + "fmt" + "io" +// "time" +) +/* +type GobInvoice struct { + Id int + CustomerId int + DepartmentId string + Raised int64 + Due int64 + Paid bool + Note string + Items []*Item +} + +func (invoice *Invoice) GobEncode() ([]byte, error) { + gobInvoice := GobInvoice{ + invoice.Id, + invoice.CustomerId, + invoice.DepartmentId, + invoice.Raised.Unix(), + invoice.Due.Unix(), + invoice.Paid, + invoice.Note, + invoice.Items, + } + var buffer bytes.Buffer + encoder := gob.NewEncoder(&buffer) + err := encoder.Encode(gobInvoice) + return buffer.Bytes(), err +} + +func (invoice *Invoice) GobDecode(data []byte) error { + var gobInvoice GobInvoice + buffer := bytes.NewBuffer(data) + decoder := gob.NewDecoder(buffer) + if err := decoder.Decode(&gobInvoice); err != nil { + return err + } + raised := time.Unix(gobInvoice.Raised, 0) + due := time.Unix(gobInvoice.Due, 0) + *invoice = Invoice{ + gobInvoice.Id, + gobInvoice.CustomerId, + gobInvoice.DepartmentId, + raised, + due, + gobInvoice.Paid, + gobInvoice.Note, + gobInvoice.Items, + } + return nil +} +*/ +type GobMarshaler struct{} + +func (GobMarshaler) MarshalInvoices(writer io.Writer, + invoices []*Invoice) error { + encoder := gob.NewEncoder(writer) + if err := encoder.Encode(magicNumber); err != nil { + return err + } + if err := encoder.Encode(fileVersion); err != nil { + return err + } + return encoder.Encode(invoices) +} + +func (GobMarshaler) UnmarshalInvoices(reader io.Reader) ([]*Invoice, + error) { + decoder := gob.NewDecoder(reader) + var magic int + if err := decoder.Decode(&magic); err != nil { + return nil, err + } + if magic != magicNumber { + return nil, errors.New("cannot read non-invoices gob file") + } + var version int + if err := decoder.Decode(&version); err != nil { + return nil, err + } + if version > fileVersion { + return nil, fmt.Errorf("version %d is too new to read", version) + } + var invoices []*Invoice + if err := decoder.Decode(&invoices); err != nil { + return nil, err + } + if version < fileVersion { + if err := update(invoices); err != nil { + return nil, err + } + } + return invoices, nil +} diff --git a/src/invoicedata_ans/inv.go b/src/invoicedata_ans/inv.go new file mode 100644 index 0000000..54ccf05 --- /dev/null +++ b/src/invoicedata_ans/inv.go @@ -0,0 +1,277 @@ +// Copyright © 2011-12 Qtrac Ltd. +// +// This program or package and any associated files are licensed under the +// Apache License, Version 2.0 (the "License"); you may not use these files +// except in compliance with the License. You can get a copy of the License +// at: http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "encoding/binary" + "errors" + "fmt" + "io" + "strconv" + "time" +) + +type InvMarshaler struct{} + +const invDateFormat = "20060102" + +var byteOrder = binary.LittleEndian + +func (InvMarshaler) MarshalInvoices(writer io.Writer, + invoices []*Invoice) error { + var write invWriterFunc = func(x interface{}) error { + return binary.Write(writer, byteOrder, x) + } + if err := write(uint32(magicNumber)); err != nil { + return err + } + if err := write(uint16(fileVersion)); err != nil { + return err + } + if err := write(int32(len(invoices))); err != nil { + return err + } + for _, invoice := range invoices { + if err := write.writeInvoice(invoice); err != nil { + return err + } + } + return nil +} + +type invWriterFunc func(interface{}) error + +func (write invWriterFunc) writeInvoice(invoice *Invoice) error { + for _, i := range []int{invoice.Id, invoice.CustomerId} { + if err := write(int32(i)); err != nil { + return err + } + } + if err := write.writeString(invoice.DepartmentId); err != nil { + return err + } + for _, date := range []time.Time{invoice.Raised, invoice.Due} { + if err := write.writeDate(date); err != nil { + return err + } + } + if err := write.writeBool(invoice.Paid); err != nil { + return err + } + if err := write.writeString(invoice.Note); err != nil { + return err + } + if err := write(int32(len(invoice.Items))); err != nil { + return err + } + for _, item := range invoice.Items { + if err := write.writeItem(item); err != nil { + return err + } + } + return nil +} + +func (write invWriterFunc) writeDate(date time.Time) error { + i, err := strconv.Atoi(date.Format(invDateFormat)) + if err != nil { + return err + } + return write(int32(i)) +} + +func (write invWriterFunc) writeBool(b bool) error { + var v int8 + if b { + v = 1 + } + return write(v) +} + +func (write invWriterFunc) writeString(s string) error { + if err := write(int32(len(s))); err != nil { + return err + } + return write([]byte(s)) +} + +func (write invWriterFunc) writeItem(item *Item) error { + if err := write.writeString(item.Id); err != nil { + return err + } + if err := write(item.Price); err != nil { + return err + } + for _, i := range []int{item.Quantity, item.TaxBand} { + if err := write(int16(i)); err != nil { + return err + } + } + return write.writeString(item.Note) +} + +func (InvMarshaler) UnmarshalInvoices(reader io.Reader) ([]*Invoice, + error) { + version, err := checkInvVersion(reader) + if err != nil { + return nil, err + } + count, err := readIntFromInt32(reader) + if err != nil { + return nil, err + } + invoices := make([]*Invoice, 0, count) + for i := 0; i < count; i++ { + invoice, err := readInvInvoice(version, reader) + if err != nil { + return nil, err + } + invoices = append(invoices, invoice) + } + return invoices, nil +} + +func readIntFromInt32(reader io.Reader) (int, error) { + var i32 int32 + err := binary.Read(reader, byteOrder, &i32) + return int(i32), err +} + +func readIntFromInt16(reader io.Reader) (int, error) { + var i16 int16 + err := binary.Read(reader, byteOrder, &i16) + return int(i16), err +} + +func readBoolFromInt8(reader io.Reader) (bool, error) { + var i8 int8 + err := binary.Read(reader, byteOrder, &i8) + return i8 == 1, err +} + +func checkInvVersion(reader io.Reader) (int, error) { + var magic uint32 + if err := binary.Read(reader, byteOrder, &magic); err != nil { + return 0, err + } + if magic != magicNumber { + return 0, errors.New("cannot read non-invoices inv file") + } + var version uint16 + if err := binary.Read(reader, byteOrder, &version); err != nil { + return 0, err + } + if version > fileVersion { + return 0, fmt.Errorf("version %d is too new to read", version) + } + return int(version), nil +} + +func readInvInvoice(version int, reader io.Reader) (invoice *Invoice, + err error) { + invoice = &Invoice{} + for _, i := range []*int{&invoice.Id, &invoice.CustomerId} { + if *i, err = readIntFromInt32(reader); err != nil { + return nil, err + } + } + if version == fileVersion { + if invoice.DepartmentId, err = readInvString(reader); err != nil { + return nil, err + } + } + for _, date := range []*time.Time{&invoice.Raised, &invoice.Due} { + if *date, err = readInvDate(reader); err != nil { + return nil, err + } + } + if invoice.Paid, err = readBoolFromInt8(reader); err != nil { + return nil, err + } + if invoice.Note, err = readInvString(reader); err != nil { + return nil, err + } + var count int + if count, err = readIntFromInt32(reader); err != nil { + return nil, err + } + if invoice.Items, err = readInvItems(version, reader, count); + err != nil { + return nil, err + } + if version < fileVersion { + updateInvoice(invoice) + } + return invoice, nil +} + +func readInvItems(version int, reader io.Reader, count int) ([]*Item, + error) { + items := make([]*Item, 0, count) + for i := 0; i < count; i++ { + item, err := readInvItem(version, reader) + if err != nil { + return nil, err + } + items = append(items, item) + } + return items, nil +} + +func readInvDate(reader io.Reader) (time.Time, error) { + var n int32 + if err := binary.Read(reader, byteOrder, &n); err != nil { + return time.Time{}, err + } + return time.Parse(invDateFormat, fmt.Sprint(n)) +} + +func readInvString(reader io.Reader) (string, error) { + var length int32 + if err := binary.Read(reader, byteOrder, &length); err != nil { + return "", err + } + raw := make([]byte, length) + if err := binary.Read(reader, byteOrder, &raw); err != nil { + return "", err + } + return string(raw), nil +} + +func readInvItem(version int, reader io.Reader) (item *Item, err error) { + item = &Item{} + if item.Id, err = readInvString(reader); err != nil { + return nil, err + } + if err = binary.Read(reader, byteOrder, &item.Price); err != nil { + return nil, err + } + if item.Quantity, err = readIntFromInt16(reader); err != nil { + return nil, err + } + if version == fileVersion { + if item.TaxBand, err = readIntFromInt16(reader); err != nil { + return nil, err + } + } + if item.Note, err = readInvString(reader); err != nil { + return nil, err + } + if version < fileVersion { + if err = updateItem(item); err != nil { + return nil, err + } + } + return item, nil +} diff --git a/src/invoicedata_ans/invoicedata.go b/src/invoicedata_ans/invoicedata.go new file mode 100644 index 0000000..4a341b4 --- /dev/null +++ b/src/invoicedata_ans/invoicedata.go @@ -0,0 +1,228 @@ +// Copyright © 2011-12 Qtrac Ltd. +// +// This program or package and any associated files are licensed under the +// Apache License, Version 2.0 (the "License"); you may not use these files +// except in compliance with the License. You can get a copy of the License +// at: http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "compress/gzip" + "errors" + "fmt" + "io" + "log" + "os" + "path/filepath" + "strconv" + "strings" + "time" +) + +const ( + magicNumber = 0x125D + fileVersion = 101 + fileType = "INVOICES" + dateFormat = "2006-01-02" // This date must always be used (see text). +) + +type Invoice struct { // fileVersion + Id int // 100 + CustomerId int // 100 + DepartmentId string // 101 + Raised time.Time // 100 + Due time.Time // 100 + Paid bool // 100 + Note string // 100 + Items []*Item // 100 +} + +type Item struct { // fileVersion + Id string // 100 + Price float64 // 100 + Quantity int // 100 + TaxBand int // 101 + Note string // 100 +} + +type InvoicesMarshaler interface { + MarshalInvoices(writer io.Writer, invoices []*Invoice) error +} + +type InvoicesUnmarshaler interface { + UnmarshalInvoices(reader io.Reader) ([]*Invoice, error) +} + +func main() { + log.SetFlags(0) + if len(os.Args) != 3 || os.Args[1] == "-h" || os.Args[1] == "--help" { + log.Fatalf("usage: %s infile.ext outfile.ext\n"+ + ".ext may be any of .gob, .inv, .jsn, .json, .txt, "+ + "or .xml, optionally gzipped (e.g., .gob.gz)\n", + filepath.Base(os.Args[0])) + } + inFilename, outFilename := os.Args[1], os.Args[2] + if inFilename == outFilename { + log.Fatalln("won't overwrite a file with itself") + } + + invoices, err := readInvoiceFile(inFilename) + if err != nil { + log.Fatalln("Failed to read:", err) + } + if err := writeInvoiceFile(outFilename, invoices); err != nil { + log.Fatalln("Failed to write:", err) + } +} + +func readInvoiceFile(filename string) ([]*Invoice, error) { + file, closer, err := openInvoiceFile(filename) + if closer != nil { + defer closer() + } + if err != nil { + return nil, err + } + return readInvoices(file, suffixOf(filename)) +} + +func openInvoiceFile(filename string) (io.ReadCloser, func(), error) { + file, err := os.Open(filename) + if err != nil { + return nil, nil, err + } + closer := func() { file.Close() } + var reader io.ReadCloser = file + var decompressor *gzip.Reader + if strings.HasSuffix(filename, ".gz") { + if decompressor, err = gzip.NewReader(file); err != nil { + return file, closer, err + } + closer = func() { decompressor.Close(); file.Close() } + reader = decompressor + } + return reader, closer, nil +} + +func readInvoices(reader io.Reader, suffix string) ([]*Invoice, error) { + var unmarshaler InvoicesUnmarshaler + switch suffix { + case ".gob": + unmarshaler = GobMarshaler{} + case ".inv": + unmarshaler = InvMarshaler{} + case ".jsn", ".json": + unmarshaler = JSONMarshaler{} + case ".txt": + unmarshaler = TxtMarshaler{} + case ".xml": + unmarshaler = XMLMarshaler{} + } + if unmarshaler != nil { + return unmarshaler.UnmarshalInvoices(reader) + } + return nil, fmt.Errorf("unrecognized input suffix: %s", suffix) +} + +func writeInvoiceFile(filename string, invoices []*Invoice) error { + file, closer, err := createInvoiceFile(filename) + if closer != nil { + defer closer() + } + if err != nil { + return err + } + return writeInvoices(file, suffixOf(filename), invoices) +} + +func createInvoiceFile(filename string) (io.WriteCloser, func(), error) { + file, err := os.Create(filename) + if err != nil { + return nil, nil, err + } + closer := func() { file.Close() } + var writer io.WriteCloser = file + var compressor *gzip.Writer + if strings.HasSuffix(filename, ".gz") { + compressor = gzip.NewWriter(file) + closer = func() { compressor.Close(); file.Close() } + writer = compressor + } + return writer, closer, nil +} + +func writeInvoices(writer io.Writer, suffix string, + invoices []*Invoice) error { + var marshaler InvoicesMarshaler + switch suffix { + case ".gob": + marshaler = GobMarshaler{} + case ".inv": + marshaler = InvMarshaler{} + case ".jsn", ".json": + marshaler = JSONMarshaler{} + case ".txt": + marshaler = TxtMarshaler{} + case ".xml": + marshaler = XMLMarshaler{} + } + if marshaler != nil { + return marshaler.MarshalInvoices(writer, invoices) + } + return errors.New("unrecognized output suffix") +} + +func update(invoices []*Invoice) error { + for _, invoice := range invoices { + updateInvoice(invoice) + for _, item := range invoice.Items { + if err := updateItem(item); err != nil { + return err + } + } + } + return nil +} + +func suffixOf(filename string) string { + suffix := filepath.Ext(filename) + if suffix == ".gz" { + suffix = filepath.Ext(filename[:len(filename)-3]) + } + return suffix +} + +func updateInvoice(invoice *Invoice) { + switch { + case invoice.Id < 3000: + invoice.DepartmentId = "GEN" + case invoice.Id < 4000: + invoice.DepartmentId = "MKT" + case invoice.Id < 5000: + invoice.DepartmentId = "COM" + case invoice.Id < 6000: + invoice.DepartmentId = "EXP" + case invoice.Id < 7000: + invoice.DepartmentId = "INP" + case invoice.Id < 8000: + invoice.DepartmentId = "TZZ" + case invoice.Id < 9000: + invoice.DepartmentId = "V20" + default: + invoice.DepartmentId = "X15" + } +} + +func updateItem(item *Item) (err error) { + if item.TaxBand, err = strconv.Atoi(item.Id[2:3]); err != nil { + return fmt.Errorf("invalid item ID: %s", item.Id) + } + return nil +} diff --git a/src/invoicedata_ans/jsn.go b/src/invoicedata_ans/jsn.go new file mode 100644 index 0000000..4095cad --- /dev/null +++ b/src/invoicedata_ans/jsn.go @@ -0,0 +1,141 @@ +// Copyright © 2011-12 Qtrac Ltd. +// +// This program or package and any associated files are licensed under the +// Apache License, Version 2.0 (the "License"); you may not use these files +// except in compliance with the License. You can get a copy of the License +// at: http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "encoding/json" + "errors" + "fmt" + "io" + "time" +) + +var version int + +type JSONInvoice struct { + Id int + CustomerId int + DepartmentId string + Raised string + Due string + Paid bool + Note string + Items []*Item +} + +type JSONInvoice100 struct { + Id int + CustomerId int + Raised string + Due string + Paid bool + Note string + Items []*Item +} + +func (invoice Invoice) MarshalJSON() ([]byte, error) { + jsonInvoice := JSONInvoice{ + invoice.Id, + invoice.CustomerId, + invoice.DepartmentId, + invoice.Raised.Format(dateFormat), + invoice.Due.Format(dateFormat), + invoice.Paid, + invoice.Note, + invoice.Items, + } + return json.Marshal(jsonInvoice) +} + +func (invoice *Invoice) UnmarshalJSON(data []byte) (err error) { + var jsonInvoice JSONInvoice + if version == fileVersion { + if err = json.Unmarshal(data, &jsonInvoice); err != nil { + return err + } + } else { + var jsonInvoice100 JSONInvoice100 + if err = json.Unmarshal(data, &jsonInvoice100); err != nil { + return err + } + jsonInvoice = JSONInvoice{ + jsonInvoice100.Id, + jsonInvoice100.CustomerId, + "", + jsonInvoice100.Raised, + jsonInvoice100.Due, + jsonInvoice100.Paid, + jsonInvoice100.Note, + jsonInvoice100.Items, + } + } + var raised, due time.Time + if raised, err = time.Parse(dateFormat, jsonInvoice.Raised); err != nil { + return err + } + if due, err = time.Parse(dateFormat, jsonInvoice.Due); err != nil { + return err + } + *invoice = Invoice{ + jsonInvoice.Id, + jsonInvoice.CustomerId, + jsonInvoice.DepartmentId, + raised, + due, + jsonInvoice.Paid, + jsonInvoice.Note, + jsonInvoice.Items, + } + return nil +} + +type JSONMarshaler struct{} + +func (JSONMarshaler) MarshalInvoices(writer io.Writer, + invoices []*Invoice) error { + encoder := json.NewEncoder(writer) + if err := encoder.Encode(fileType); err != nil { + return err + } + if err := encoder.Encode(fileVersion); err != nil { + return err + } + return encoder.Encode(invoices) +} + +func (JSONMarshaler) UnmarshalInvoices(reader io.Reader) ([]*Invoice, + error) { + decoder := json.NewDecoder(reader) + var kind string + if err := decoder.Decode(&kind); err != nil { + return nil, err + } + if kind != fileType { + return nil, errors.New("cannot read non-invoices json file") + } + if err := decoder.Decode(&version); err != nil { + return nil, err + } + if version > fileVersion { + return nil, fmt.Errorf("version %d is too new to read", version) + } + var invoices []*Invoice + if err := decoder.Decode(&invoices); err != nil { + return nil, err + } + if err := update(invoices); err != nil { + return nil, err + } + return invoices, nil +} diff --git a/src/invoicedata_ans/txt.go b/src/invoicedata_ans/txt.go new file mode 100644 index 0000000..7879242 --- /dev/null +++ b/src/invoicedata_ans/txt.go @@ -0,0 +1,195 @@ +// Copyright © 2011-12 Qtrac Ltd. +// +// This program or package and any associated files are licensed under the +// Apache License, Version 2.0 (the "License"); you may not use these files +// except in compliance with the License. You can get a copy of the License +// at: http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "bufio" + "errors" + "fmt" + "io" + "strings" + "time" +) + +const noteSep = ":" + +type TxtMarshaler struct{} + +func (TxtMarshaler) MarshalInvoices(writer io.Writer, + invoices []*Invoice) error { + bufferedWriter := bufio.NewWriter(writer) + defer bufferedWriter.Flush() + var write writerFunc = func(format string, + args ...interface{}) error { + _, err := fmt.Fprintf(bufferedWriter, format, args...) + return err + } + if err := write("%s %d\n", fileType, fileVersion); err != nil { + return err + } + for _, invoice := range invoices { + if err := write.writeInvoice(invoice); err != nil { + return err + } + } + return nil +} + +type writerFunc func(string, ...interface{}) error + +func (write writerFunc) writeInvoice(invoice *Invoice) error { + note := "" + if invoice.Note != "" { + note = noteSep + " " + invoice.Note + } + if err := write("INVOICE ID=%d CUSTOMER=%d DEPARTMENT=%s RAISED=%s "+ + "DUE=%s PAID=%t%s\n", invoice.Id, invoice.CustomerId, + invoice.DepartmentId, invoice.Raised.Format(dateFormat), + invoice.Due.Format(dateFormat), invoice.Paid, note); err != nil { + return err + } + if err := write.writeItems(invoice.Items); err != nil { + return err + } + return write("\f\n") +} + +func (write writerFunc) writeItems(items []*Item) error { + for _, item := range items { + note := "" + if item.Note != "" { + note = noteSep + " " + item.Note + } + if err := write("ITEM ID=%s PRICE=%.2f QUANTITY=%d TAXBAND=%d%s\n", + item.Id, item.Price, item.Quantity, item.TaxBand, note); + err != nil { + return err + } + } + return nil +} + +func (TxtMarshaler) UnmarshalInvoices(reader io.Reader) ( + invoices []*Invoice, err error) { + bufferedReader := bufio.NewReader(reader) + var version int + if version, err = checkTxtVersion(bufferedReader); err != nil { + return nil, err + } + var line string + eof := false + for lino := 2; !eof; lino++ { + line, err = bufferedReader.ReadString('\n') + if err == io.EOF { + err = nil + eof = true + } else if err != nil { + return nil, err + } + if invoices, err = parseTxtLine(version, lino, line, invoices); + err != nil { + return nil, err + } + } + return invoices, nil +} + +func checkTxtVersion(bufferedReader *bufio.Reader) (version int, + err error) { + if _, err = fmt.Fscanf(bufferedReader, "INVOICES %d\n", &version); + err != nil { + err = errors.New("cannot read non-invoices text file") + } else if version > fileVersion { + err = fmt.Errorf("version %d is too new to read", version) + } + return version, err +} + +func parseTxtLine(version, lino int, line string, invoices []*Invoice) ( + []*Invoice, error) { + var err error + if strings.HasPrefix(line, "INVOICE") { + var invoice *Invoice + invoice, err = parseTxtInvoice(version, lino, line) + invoices = append(invoices, invoice) + } else if strings.HasPrefix(line, "ITEM") { + if len(invoices) == 0 { + err = fmt.Errorf("item outside of an invoice line %d", lino) + } else { + var item *Item + item, err = parseTxtItem(version, lino, line) + items := &invoices[len(invoices)-1].Items + *items = append(*items, item) + } + } + return invoices, err +} + +func parseTxtInvoice(version, lino int, line string) (invoice *Invoice, + err error) { + invoice = &Invoice{} + var raised, due string + if version == fileVersion { + if _, err = fmt.Sscanf(line, "INVOICE ID=%d CUSTOMER=%d "+ + "DEPARTMENT=%s RAISED=%s DUE=%s PAID=%t", &invoice.Id, + &invoice.CustomerId, &invoice.DepartmentId, &raised, &due, + &invoice.Paid); err != nil { + return nil, fmt.Errorf("invalid invoice %v line %d", err, lino) + } + } else { + if _, err = fmt.Sscanf(line, "INVOICE ID=%d CUSTOMER=%d "+ + "RAISED=%s DUE=%s PAID=%t", &invoice.Id, + &invoice.CustomerId, &raised, &due, &invoice.Paid); + err != nil { + return nil, fmt.Errorf("invalid invoice %v line %d", err, lino) + } + } + if invoice.Raised, err = time.Parse(dateFormat, raised); err != nil { + return nil, fmt.Errorf("invalid raised %v line %d", err, lino) + } + if invoice.Due, err = time.Parse(dateFormat, due); err != nil { + return nil, fmt.Errorf("invalid due %v line %d", err, lino) + } + if i := strings.Index(line, noteSep); i > -1 { + invoice.Note = strings.TrimSpace(line[i+len(noteSep):]) + } + if version < fileVersion { + updateInvoice(invoice) + } + return invoice, nil +} + +func parseTxtItem(version, lino int, line string) (item *Item, + err error) { + item = &Item{} + if version == fileVersion { + if _, err = fmt.Sscanf(line, "ITEM ID=%s PRICE=%f "+ + "QUANTITY=%d TAXBAND=%d", &item.Id, &item.Price, + &item.Quantity, &item.TaxBand); err != nil { + return nil, fmt.Errorf("invalid item %v line %d", err, lino) + } + } else { + if _, err = fmt.Sscanf(line, "ITEM ID=%s PRICE=%f QUANTITY=%d", + &item.Id, &item.Price, &item.Quantity); err != nil { + return nil, fmt.Errorf("invalid item %v line %d", err, lino) + } + } + if i := strings.Index(line, noteSep); i > -1 { + item.Note = strings.TrimSpace(line[i+len(noteSep):]) + } + if version < fileVersion { + updateItem(item) + } + return item, nil +} diff --git a/src/invoicedata_ans/xml.go b/src/invoicedata_ans/xml.go new file mode 100644 index 0000000..e0d21b4 --- /dev/null +++ b/src/invoicedata_ans/xml.go @@ -0,0 +1,181 @@ +// Copyright © 2011-12 Qtrac Ltd. +// +// This program or package and any associated files are licensed under the +// Apache License, Version 2.0 (the "License"); you may not use these files +// except in compliance with the License. You can get a copy of the License +// at: http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "encoding/xml" + "fmt" + "io" + "strings" + "time" +) + +type XMLMarshaler struct{} + +type XMLInvoices struct { + XMLName xml.Name `xml:"INVOICES"` + Version int `xml:"version,attr"` + Invoice []*XMLInvoice `xml:"INVOICE"` +} + +type XMLInvoice struct { + XMLName xml.Name `xml:"INVOICE"` + Id int `xml:",attr"` + CustomerId int `xml:",attr"` + DepartmentId string `xml:",attr"` + Raised string `xml:",attr"` + Due string `xml:",attr"` + Paid bool `xml:",attr"` + Note string `xml:"NOTE"` + Item []*XMLItem `xml:"ITEM"` +} + +type XMLItem struct { + XMLName xml.Name `xml:"ITEM"` + Id string `xml:",attr"` + Price float64 `xml:",attr"` + Quantity int `xml:",attr"` + TaxBand int `xml:",attr"` + Note string `xml:"NOTE"` +} + + +func XMLInvoicesForInvoices(invoices []*Invoice) *XMLInvoices { + xmlInvoices := &XMLInvoices{ + Version: fileVersion, + Invoice: make([]*XMLInvoice, 0, len(invoices)), + } + for _, invoice := range invoices { + xmlInvoices.Invoice = append(xmlInvoices.Invoice, + XMLInvoiceForInvoice(invoice)) + } + return xmlInvoices +} + + +func XMLInvoiceForInvoice(invoice *Invoice) *XMLInvoice { + xmlInvoice := &XMLInvoice{ + Id: invoice.Id, + CustomerId: invoice.CustomerId, + DepartmentId: invoice.DepartmentId, + Raised: invoice.Raised.Format(dateFormat), + Due: invoice.Due.Format(dateFormat), + Paid: invoice.Paid, + Note: invoice.Note, + Item: make([]*XMLItem, 0, len(invoice.Items)), + } + for _, item := range invoice.Items { + xmlItem := &XMLItem{ + Id: item.Id, + Price: item.Price, + Quantity: item.Quantity, + TaxBand: item.TaxBand, + Note: item.Note, + } + xmlInvoice.Item = append(xmlInvoice.Item, xmlItem) + } + return xmlInvoice +} + + +func (xmlInvoices *XMLInvoices) Invoices(version int) (invoices []*Invoice, + err error) { + invoices = make([]*Invoice, 0, len(xmlInvoices.Invoice)) + for _, xmlInvoice := range xmlInvoices.Invoice { + invoice, err := xmlInvoice.Invoice(version) + if err != nil { + return nil, err + } + invoices = append(invoices, invoice) + } + return invoices, nil +} + + +func (xmlInvoice *XMLInvoice) Invoice(version int) (invoice *Invoice, + err error) { + invoice = &Invoice{ + Id: xmlInvoice.Id, + CustomerId: xmlInvoice.CustomerId, + Paid: xmlInvoice.Paid, + Note: strings.TrimSpace(xmlInvoice.Note), + Items: make([]*Item, 0, len(xmlInvoice.Item)), + } + if version == fileVersion { + invoice.DepartmentId = xmlInvoice.DepartmentId + } + if invoice.Raised, err = time.Parse(dateFormat, xmlInvoice.Raised); + err != nil { + return nil, err + } + if invoice.Due, err = time.Parse(dateFormat, xmlInvoice.Due); + err != nil { + return nil, err + } + if invoice.Items, err = itemsForXMLItems(version, xmlInvoice.Item); + err != nil { + return nil, err + } + if version < fileVersion { + updateInvoice(invoice) + } + return invoice, nil +} + +func itemsForXMLItems(version int, xmlItems []*XMLItem) (items []*Item, + err error) { + for _, xmlItem := range xmlItems { + item := &Item{ + Id: xmlItem.Id, + Price: xmlItem.Price, + Quantity: xmlItem.Quantity, + Note: strings.TrimSpace(xmlItem.Note), + } + if version == fileVersion { + item.TaxBand = xmlItem.TaxBand + } else if version < fileVersion { + if err = updateItem(item); err != nil { + return nil, err + } + } + items = append(items, item) + } + return items, nil +} + + +func (XMLMarshaler) MarshalInvoices(writer io.Writer, + invoices []*Invoice) error { + if _, err := writer.Write([]byte(xml.Header)); err != nil { + return err + } + xmlInvoices := XMLInvoicesForInvoices(invoices) + encoder := xml.NewEncoder(writer) + return encoder.Encode(xmlInvoices) +} + + +func (XMLMarshaler) UnmarshalInvoices(reader io.Reader) ([]*Invoice, + error) { + xmlInvoices := &XMLInvoices{} + decoder := xml.NewDecoder(reader) + if err := decoder.Decode(xmlInvoices); err != nil { + return nil, err + } + if xmlInvoices.Version > fileVersion { + return nil, fmt.Errorf("version %d is too new to read", + xmlInvoices.Version) + } + return xmlInvoices.Invoices(xmlInvoices.Version) +} diff --git a/src/linkcheck/linkcheck.go b/src/linkcheck/linkcheck.go new file mode 100644 index 0000000..ab0aab8 --- /dev/null +++ b/src/linkcheck/linkcheck.go @@ -0,0 +1,164 @@ +// Copyright © 2011-12 Qtrac Ltd. +// +// This program or package and any associated files are licensed under the +// Apache License, Version 2.0 (the "License"); you may not use these files +// except in compliance with the License. You can get a copy of the License +// at: http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "fmt" + "linkcheck/linkutil" + "log" + "net/http" + "net/url" + "os" + "path/filepath" + "regexp" + "strings" +) + +var ( + externalLinkRx *regexp.Regexp + addChannel chan string + queryChannel chan string + seenChannel chan bool +) + +func init() { + externalLinkRx = regexp.MustCompile("^(http|ftp|mailto):") + // These *must* be unbuffered so that they block and properly serialize + // access to the map + addChannel = make(chan string) + queryChannel = make(chan string) + seenChannel = make(chan bool) +} + +func main() { + log.SetFlags(0) + if len(os.Args) != 2 || os.Args[1] == "-h" || os.Args[1] == "--help" { + log.Fatalf("usage: %s url\n", filepath.Base(os.Args[0])) + } + href := os.Args[1] + if !strings.HasPrefix(href, "http://") { + href = "http://" + href + } + url, err := url.Parse(href) + if err != nil { + log.Fatalln("- failed to read url:", err) + } + prepareMap() + checkPage(href, "http://"+url.Host) +} + +func prepareMap() { + go func() { + seen := make(map[string]bool) + for { + select { + case url := <-addChannel: + seen[url] = true + case url := <-queryChannel: + _, found := seen[url] + seenChannel <- found + } + } + }() +} + +func alreadySeen(url string) bool { + queryChannel <- url + if <-seenChannel { + return true + } + addChannel <- url + return false +} + +func checkPage(url, site string) { + if alreadySeen(url) { + return + } + links, err := linkutil.LinksFromURL(url) + if err != nil { + log.Println("-", err) + return + } + fmt.Println("+ read", url) + done := make(chan bool, len(links)) + defer close(done) + pending := 0 + var messages []string + for _, link := range links { + pending += processLink(link, site, url, &messages, done) + } + if len(messages) > 0 { + fmt.Println("+ links on", url) + for _, message := range messages { + fmt.Println(" ", message) + } + } + for i := 0; i < pending; i++ { + <-done + } +} + +func processLink(link, site, url string, messages *[]string, + done chan<- bool) int { + localAndParsable, link := classifyLink(link, site) + if localAndParsable { + go func() { + checkPage(link, site) + done <- true + }() + return 1 + } + if message := checkExists(link, url); message != "" { + *messages = append(*messages, message) + } + return 0 +} + +func classifyLink(link, site string) (bool, string) { + var local, parsable bool + url := link + lowerLink := strings.ToLower(link) + if strings.HasSuffix(lowerLink, ".htm") || + strings.HasSuffix(lowerLink, ".html") { + parsable = true + } + if !externalLinkRx.MatchString(link) { + local = true + url = site + if link[0] != '/' && url[len(url)-1] != '/' { + url += "/" + } + url += link + } + return local && parsable, url +} + +func checkExists(link, url string) string { + if alreadySeen(link) { + return "" + } + if _, err := http.Head(link); err != nil { + errStr := err.Error() + if strings.Contains(errStr, "unsupported protocol scheme") { + return fmt.Sprint("- can't check nonhttp link: ", link) + } else if strings.Contains(errStr, "connection timed out") { + return fmt.Sprint("- timed out on: ", link) + } else { + return fmt.Sprintf("- bad link %s on page %s: %v", link, url, + err) + } + } + return fmt.Sprint("+ checked ", link) +} diff --git a/src/linkcheck/linkutil/index.html b/src/linkcheck/linkutil/index.html new file mode 100644 index 0000000..44601e7 --- /dev/null +++ b/src/linkcheck/linkutil/index.html @@ -0,0 +1,124 @@ + + + + + + + + Qtrac Ltd. + + + + + + + + + + + +

Qtrac Ltd.

+

Quality Training Research and Consultancy

+ + +

+Qtrac Ltd. (incorporating Qtraining.eu) provides training, consultancy, +programming, and documentation services, specializing in the programming +languages Python and C++, and in the Qt +application development framework—both PyQt4 and C++/Qt 4. All +services are provided by author and programmer Mark Summerfield (mark@qtrac.eu). +

+ +

Services

+ +
    +
  • Consultancy, code reviews, mentoring, programming, and training in: +
      +
    • C++, Python 2, Python 3, and Go programming; +
    • C++/Qt 4, PyQt4, and PySide GUI programming; +
    • Squish GUI application +testing; +
    • Regular expressions.
    • +
    +
  • Technical authoring and editing.
  • +
  • Technical book and document typesetting using the lout typesetting +system.
  • +
+ +

Writing

+ +

+Our books are listed on the right. +Our most recent publication is +Advanced Qt Programming. +We are currently writing +Programming in Go: Creating Applications for the +21st Century. +We also have a free download, +Moving from Python 2 to Python 3 (4 pages, 676K PDF)—a very +concise summary of Python 2 ↔ 3.1 differences plus +new Python 3.1/3.2 features. +

+ +

Free Open Source Software

+ +

+We maintain three free and open source GUI programs, the most popular of +which is DiffPDF. This is a GUI application +for comparing PDF files page by page, either textually or by their +appearance. Available prebuilt for Debian, Fedora, Ubuntu, +Mac OS X, and Windows, and in source form for other platforms. +

+ +

Top


+ + diff --git a/src/linkcheck/linkutil/index.links b/src/linkcheck/linkutil/index.links new file mode 100644 index 0000000..c59b877 --- /dev/null +++ b/src/linkcheck/linkutil/index.links @@ -0,0 +1,18 @@ +#top +about.html +alt_key.html +alt_key_api.html +aqpbook.html +comparepdf.html +diffpdf.html +gobook.html +http://code.google.com/p/optarg/ +http://endsoftpatents.org/innovating-without-patents +http://golang.org +http://ptgmedia.pearsoncmg.com/imprint_downloads/informit/promotions/python/python2python3.pdf +http://savannah.nongnu.org/projects/lout/ +http://www.froglogic.com +mailto:mark@qtrac.eu +marksummerfield.html +py3book.html +pyqtbook.html diff --git a/src/linkcheck/linkutil/linkutil.go b/src/linkcheck/linkutil/linkutil.go new file mode 100644 index 0000000..7b0a7d0 --- /dev/null +++ b/src/linkcheck/linkutil/linkutil.go @@ -0,0 +1,62 @@ +// Copyright © 2011-12 Qtrac Ltd. +// +// This program or package and any associated files are licensed under the +// Apache License, Version 2.0 (the "License"); you may not use these files +// except in compliance with the License. You can get a copy of the License +// at: http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package linkutil + +import ( + "fmt" + "io" + "io/ioutil" + "net/http" + "regexp" +) + +var hrefRx *regexp.Regexp + +func init() { + hrefRx = regexp.MustCompile(`]+href=['"]?([^'">]+)['"]?`) +} + +func LinksFromURL(url string) ([]string, error) { + response, err := http.Get(url) + if err != nil { + return nil, fmt.Errorf("failed to get page: %s", err) + } + links, err := LinksFromReader(response.Body) + if err != nil { + return nil, fmt.Errorf("failed to parse page: %s", err) + } + return links, nil +} + +func LinksFromReader(reader io.Reader) ([]string, error) { + html, err := ioutil.ReadAll(reader) + if err != nil { + return nil, err + } + // FindAllSubmatch returns a slice of slices of slices! + // The outer level is each match, the next level is the groups, 0 for + // the whole match 1 for first set of ()s etc; the innermost level + // being individual () matches + uniqueLinks := make(map[string]bool) + for _, submatch := range hrefRx.FindAllSubmatch(html, -1) { + uniqueLinks[string(submatch[1])] = true + } + links := make([]string, len(uniqueLinks)) + i := 0 + for link := range uniqueLinks { + links[i] = link + i++ + } + return links, nil +} diff --git a/src/linkcheck/linkutil/linkutil_test.go b/src/linkcheck/linkutil/linkutil_test.go new file mode 100644 index 0000000..85ed39b --- /dev/null +++ b/src/linkcheck/linkutil/linkutil_test.go @@ -0,0 +1,70 @@ +// Copyright © 2011-12 Qtrac Ltd. +// +// This program or package and any associated files are licensed under the +// Apache License, Version 2.0 (the "License"); you may not use these files +// except in compliance with the License. You can get a copy of the License +// at: http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package linkutil_test + +import ( + "bufio" + "io" + "linkcheck/linkutil" + "os" + "reflect" + "sort" + "testing" +) + +func TestLinksFromReader(t *testing.T) { + file, err := os.Open("index.html") + if err != nil { + t.Fatal(err) + } + defer file.Close() + links, err := linkutil.LinksFromReader(file) + if err != nil { + t.Fatal(err) + } + sort.Strings(links) + file, err = os.Open("index.links") + if err != nil { + t.Fatal(err) + } + defer file.Close() + reader := bufio.NewReader(file) + var lines []string + for { + line, err := reader.ReadString('\n') + if line != "" { + lines = append(lines, line[:len(line)-1]) + } + if err != nil { + if err != io.EOF { + t.Fatal(err) + } + break + } + } + sort.Strings(lines) + if !reflect.DeepEqual(links, lines) { + for i := 0; i < len(links); i++ { + if i < len(lines) { + if links[i] != lines[i] { + t.Fatalf("%q != %q", links[i], lines[i]) + } + } else { + t.Fatalf("found more links than lines, e.g.: %q", links[i]) + } + } + t.Fatalf("found fewer links than lines (%d vs. %d)", len(links), + len(lines)) + } +} diff --git a/src/m3u2pls/David-Bowie-Singles.m3u b/src/m3u2pls/David-Bowie-Singles.m3u new file mode 100644 index 0000000..96e260a --- /dev/null +++ b/src/m3u2pls/David-Bowie-Singles.m3u @@ -0,0 +1,67 @@ +#EXTM3U +#EXTINF:315,David Bowie - Space Oddity +Music/David Bowie/Singles 1/01-Space Oddity.ogg +#EXTINF:-1,David Bowie - Changes +Music/David Bowie/Singles 1/02-Changes.ogg +#EXTINF:258,David Bowie - Starman +Music/David Bowie/Singles 1/03-Starman.ogg +#EXTINF:194,David Bowie - Ziggy Stardust +Music/David Bowie/Singles 1/04-Ziggy Stardust.ogg +#EXTINF:206,David Bowie - Suffragette City +Music/David Bowie/Singles 1/05-Suffragette City.ogg +#EXTINF:247,David Bowie - The Jean Genie +Music/David Bowie/Singles 1/07-The Jean Genie.ogg +#EXTINF:231,David Bowie - Life On Mars? +Music/David Bowie/Singles 1/09-Life On Mars.ogg +#EXTINF:174,David Bowie - Sorrow +Music/David Bowie/Singles 1/10-Sorrow.ogg +#EXTINF:270,David Bowie - Rebel Rebel +Music/David Bowie/Singles 1/11-Rebel Rebel.ogg +#EXTINF:178,David Bowie - Rock & Roll Suicide +Music/David Bowie/Singles 1/12-Rock and Roll Suicide.ogg +#EXTINF:364,David Bowie - Diamond Dogs +Music/David Bowie/Singles 1/13-Diamond Dogs.ogg +#EXTINF:311,David Bowie - Young Americans +Music/David Bowie/Singles 1/15-Young Americans.ogg +#EXTINF:254,David Bowie - Fame +Music/David Bowie/Singles 1/16-Fame.ogg +#EXTINF:240,David Bowie - Golden Years +Music/David Bowie/Singles 1/17-Golden Years.ogg +#EXTINF:331,David Bowie - TVC 15 +Music/David Bowie/Singles 1/18-TVC 15.ogg +#EXTINF:183,David Bowie - Sound & Vision +Music/David Bowie/Singles 1/19-Sound & Vision.ogg +#EXTINF:217,David Bowie - Heroes +Music/David Bowie/Singles 2/01-Heroes.ogg +#EXTINF:214,David Bowie - Beauty & The Beast +Music/David Bowie/Singles 2/02-Beauty and The Beast.ogg +#EXTINF:197,David Bowie - Boys Keep Swinging +Music/David Bowie/Singles 2/03-Boys Keep Swinging.ogg +#EXTINF:240,David Bowie - D.J. +Music/David Bowie/Singles 2/04-D.J..ogg +#EXTINF:-1,David Bowie - Alabama Song +Music/David Bowie/Singles 2/05-Alabama Song.ogg +#EXTINF:264,David Bowie - Ashes To Ashes +Music/David Bowie/Singles 2/06-Ashes To Ashes.ogg +#EXTINF:287,David Bowie - Fashion +Music/David Bowie/Singles 2/07-Fashion.ogg +#EXTINF:311,David Bowie - Scary Monsters (And Super Creeps) +Music/David Bowie/Singles 2/08-Scary Monsters (And Super Creeps).ogg +#EXTINF:237,David Bowie - Under Pressure +Music/David Bowie/Singles 2/09-Under Pressure.ogg +#EXTINF:361,David Bowie - Wild Is The Wind +Music/David Bowie/Singles 2/10-Wild Is The Wind.ogg +#EXTINF:250,David Bowie - Let's Dance +Music/David Bowie/Singles 2/11-Let's Dance.ogg +#EXTINF:256,David Bowie - China Girl +Music/David Bowie/Singles 2/12-China Girl.ogg +#EXTINF:237,David Bowie - Modern Love +Music/David Bowie/Singles 2/13-Modern Love.ogg +#EXTINF:191,David Bowie - Blue Jean +Music/David Bowie/Singles 2/14-Blue Jean.ogg +#EXTINF:230,David Bowie - This Is Not America +Music/David Bowie/Singles 2/15-This Is Not America.ogg +#EXTINF:337,David Bowie - Absolute Beginners +Music/David Bowie/Singles 2/17-Absolute Beginners.ogg +#EXTINF:251,David Bowie - Day In Day Out +Music/David Bowie/Singles 2/18-Day In Day Out.ogg diff --git a/src/m3u2pls/David-Bowie-Singles.pls b/src/m3u2pls/David-Bowie-Singles.pls new file mode 100644 index 0000000..0dcebfd --- /dev/null +++ b/src/m3u2pls/David-Bowie-Singles.pls @@ -0,0 +1,102 @@ +[playlist] +File1=Music/David Bowie/Singles 1/01-Space Oddity.ogg +Title1=David Bowie - Space Oddity +Length1=315 +File2=Music/David Bowie/Singles 1/02-Changes.ogg +Title2=David Bowie - Changes +Length2=-1 +File3=Music/David Bowie/Singles 1/03-Starman.ogg +Title3=David Bowie - Starman +Length3=258 +File4=Music/David Bowie/Singles 1/04-Ziggy Stardust.ogg +Title4=David Bowie - Ziggy Stardust +Length4=194 +File5=Music/David Bowie/Singles 1/05-Suffragette City.ogg +Title5=David Bowie - Suffragette City +Length5=206 +File6=Music/David Bowie/Singles 1/07-The Jean Genie.ogg +Title6=David Bowie - The Jean Genie +Length6=247 +File7=Music/David Bowie/Singles 1/09-Life On Mars.ogg +Title7=David Bowie - Life On Mars? +Length7=231 +File8=Music/David Bowie/Singles 1/10-Sorrow.ogg +Title8=David Bowie - Sorrow +Length8=174 +File9=Music/David Bowie/Singles 1/11-Rebel Rebel.ogg +Title9=David Bowie - Rebel Rebel +Length9=270 +File10=Music/David Bowie/Singles 1/12-Rock and Roll Suicide.ogg +Title10=David Bowie - Rock & Roll Suicide +Length10=178 +File11=Music/David Bowie/Singles 1/13-Diamond Dogs.ogg +Title11=David Bowie - Diamond Dogs +Length11=364 +File12=Music/David Bowie/Singles 1/15-Young Americans.ogg +Title12=David Bowie - Young Americans +Length12=311 +File13=Music/David Bowie/Singles 1/16-Fame.ogg +Title13=David Bowie - Fame +Length13=254 +File14=Music/David Bowie/Singles 1/17-Golden Years.ogg +Title14=David Bowie - Golden Years +Length14=240 +File15=Music/David Bowie/Singles 1/18-TVC 15.ogg +Title15=David Bowie - TVC 15 +Length15=331 +File16=Music/David Bowie/Singles 1/19-Sound & Vision.ogg +Title16=David Bowie - Sound & Vision +Length16=183 +File17=Music/David Bowie/Singles 2/01-Heroes.ogg +Title17=David Bowie - Heroes +Length17=217 +File18=Music/David Bowie/Singles 2/02-Beauty and The Beast.ogg +Title18=David Bowie - Beauty & The Beast +Length18=214 +File19=Music/David Bowie/Singles 2/03-Boys Keep Swinging.ogg +Title19=David Bowie - Boys Keep Swinging +Length19=197 +File20=Music/David Bowie/Singles 2/04-D.J..ogg +Title20=David Bowie - D.J. +Length20=240 +File21=Music/David Bowie/Singles 2/05-Alabama Song.ogg +Title21=David Bowie - Alabama Song +Length21=-1 +File22=Music/David Bowie/Singles 2/06-Ashes To Ashes.ogg +Title22=David Bowie - Ashes To Ashes +Length22=264 +File23=Music/David Bowie/Singles 2/07-Fashion.ogg +Title23=David Bowie - Fashion +Length23=287 +File24=Music/David Bowie/Singles 2/08-Scary Monsters (And Super Creeps).ogg +Title24=David Bowie - Scary Monsters (And Super Creeps) +Length24=311 +File25=Music/David Bowie/Singles 2/09-Under Pressure.ogg +Title25=David Bowie - Under Pressure +Length25=237 +File26=Music/David Bowie/Singles 2/10-Wild Is The Wind.ogg +Title26=David Bowie - Wild Is The Wind +Length26=361 +File27=Music/David Bowie/Singles 2/11-Let's Dance.ogg +Title27=David Bowie - Let's Dance +Length27=250 +File28=Music/David Bowie/Singles 2/12-China Girl.ogg +Title28=David Bowie - China Girl +Length28=256 +File29=Music/David Bowie/Singles 2/13-Modern Love.ogg +Title29=David Bowie - Modern Love +Length29=237 +File30=Music/David Bowie/Singles 2/14-Blue Jean.ogg +Title30=David Bowie - Blue Jean +Length30=191 +File31=Music/David Bowie/Singles 2/15-This Is Not America.ogg +Title31=David Bowie - This Is Not America +Length31=230 +File32=Music/David Bowie/Singles 2/17-Absolute Beginners.ogg +Title32=David Bowie - Absolute Beginners +Length32=337 +File33=Music/David Bowie/Singles 2/18-Day In Day Out.ogg +Title33=David Bowie - Day In Day Out +Length33=251 +NumberOfEntries=33 +Version=2 diff --git a/src/m3u2pls/m3u2pls.go b/src/m3u2pls/m3u2pls.go new file mode 100644 index 0000000..0393905 --- /dev/null +++ b/src/m3u2pls/m3u2pls.go @@ -0,0 +1,99 @@ +// Copyright © 2011-12 Qtrac Ltd. +// +// This program or package and any associated files are licensed under the +// Apache License, Version 2.0 (the "License"); you may not use these files +// except in compliance with the License. You can get a copy of the License +// at: http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "fmt" + "io/ioutil" + "log" + "os" + "path/filepath" + "strconv" + "strings" +) + +type Song struct { + Title string + Filename string + Seconds int +} + +func main() { + if len(os.Args) == 1 || !strings.HasSuffix(os.Args[1], ".m3u") { + fmt.Printf("usage: %s \n", filepath.Base(os.Args[0])) + os.Exit(1) + } + + if rawBytes, err := ioutil.ReadFile(os.Args[1]); err != nil { + log.Fatal(err) + } else { + songs := readM3uPlaylist(string(rawBytes)) + writePlsPlaylist(songs) + } +} + +func readM3uPlaylist(data string) (songs []Song) { + var song Song + for _, line := range strings.Split(data, "\n") { + line = strings.TrimSpace(line) + if line == "" || strings.HasPrefix(line, "#EXTM3U") { + continue + } + if strings.HasPrefix(line, "#EXTINF:") { + song.Title, song.Seconds = parseExtinfLine(line) + } else { + song.Filename = strings.Map(mapPlatformDirSeparator, line) + } + if song.Filename != "" && song.Title != "" && song.Seconds != 0 { + songs = append(songs, song) + song = Song{} + } + } + return songs +} + +func parseExtinfLine(line string) (title string, seconds int) { + if i := strings.IndexAny(line, "-0123456789"); i > -1 { + const separator = "," + line = line[i:] + if j := strings.Index(line, separator); j > -1 { + title = line[j+len(separator):] + var err error + if seconds, err = strconv.Atoi(line[:j]); err != nil { + log.Printf("failed to read the duration for '%s': %v\n", + title, err) + seconds = -1 + } + } + } + return title, seconds +} + +func mapPlatformDirSeparator(char rune) rune { + if char == '/' || char == '\\' { + return filepath.Separator + } + return char +} + +func writePlsPlaylist(songs []Song) { + fmt.Println("[playlist]") + for i, song := range songs { + i++ + fmt.Printf("File%d=%s\n", i, song.Filename) + fmt.Printf("Title%d=%s\n", i, song.Title) + fmt.Printf("Length%d=%d\n", i, song.Seconds) + } + fmt.Printf("NumberOfEntries=%d\nVersion=2\n", len(songs)) +} diff --git a/src/m3u2pls/m3u2pls_test.go b/src/m3u2pls/m3u2pls_test.go new file mode 100644 index 0000000..80102dc --- /dev/null +++ b/src/m3u2pls/m3u2pls_test.go @@ -0,0 +1,92 @@ +// Copyright © 2011-12 Qtrac Ltd. +// +// This program or package and any associated files are licensed under the +// Apache License, Version 2.0 (the "License"); you may not use these files +// except in compliance with the License. You can get a copy of the License +// at: http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "io/ioutil" + "log" + "os" + "testing" +) + +func TestReadM3uPlaylist(t *testing.T) { + log.SetFlags(0) + log.Println("TEST m3u2pls") + + songs := readM3uPlaylist(M3U) + for i, song := range songs { + if song.Title != ExpectedSongs[i].Title { + t.Fatalf("%q != %q", song.Title, ExpectedSongs[i].Title) + } + if song.Filename != ExpectedSongs[i].Filename { + t.Fatalf("%q != %q", song.Filename, + ExpectedSongs[i].Filename) + } + if song.Seconds != ExpectedSongs[i].Seconds { + t.Fatalf("%d != %d", song.Seconds, + ExpectedSongs[i].Seconds) + } + } +} + +func TestWritePlsPlaylist(t *testing.T) { + songs := readM3uPlaylist(M3U) + var err error + reader, writer := os.Stdin, os.Stdout + if reader, writer, err = os.Pipe(); err != nil { + t.Fatal(err) + } + os.Stdout = writer + writePlsPlaylist(songs) + writer.Close() + actual, err := ioutil.ReadAll(reader) + if err != nil { + t.Fatal(err) + } + reader.Close() + if string(actual) != ExpectedPls { + t.Fatal("actual != expected") + } +} + +const M3U = `#EXTM3U +#EXTINF:315,David Bowie - Space Oddity +Music/David Bowie/Singles 1/01-Space Oddity.ogg +#EXTINF:-1,David Bowie - Changes +Music/David Bowie/Singles 1/02-Changes.ogg +#EXTINF:258,David Bowie - Starman +Music/David Bowie/Singles 1/03-Starman.ogg` + +var ExpectedSongs = []Song{ + {"David Bowie - Space Oddity", + "Music/David Bowie/Singles 1/01-Space Oddity.ogg", 315}, + {"David Bowie - Changes", + "Music/David Bowie/Singles 1/02-Changes.ogg", -1}, + {"David Bowie - Starman", + "Music/David Bowie/Singles 1/03-Starman.ogg", 258}, + } + +var ExpectedPls = `[playlist] +File1=Music/David Bowie/Singles 1/01-Space Oddity.ogg +Title1=David Bowie - Space Oddity +Length1=315 +File2=Music/David Bowie/Singles 1/02-Changes.ogg +Title2=David Bowie - Changes +Length2=-1 +File3=Music/David Bowie/Singles 1/03-Starman.ogg +Title3=David Bowie - Starman +Length3=258 +NumberOfEntries=3 +Version=2 +` diff --git a/src/memoize/memoize.go b/src/memoize/memoize.go new file mode 100644 index 0000000..a4b6842 --- /dev/null +++ b/src/memoize/memoize.go @@ -0,0 +1,81 @@ +// Copyright © 2011-12 Qtrac Ltd. +// +// This program or package and any associated files are licensed under the +// Apache License, Version 2.0 (the "License"); you may not use these files +// except in compliance with the License. You can get a copy of the License +// at: http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "bytes" + "fmt" + "strings" +) + +type memoizeFunction func(int, ...int) interface{} + +var Fibonacci memoizeFunction +var RomanForDecimal memoizeFunction + +func init() { + // Of course the iterative version of fibonacci (that doesn't need + // memoize) is much more efficient. + Fibonacci = Memoize(func(x int, xs ...int) interface{} { + if x < 2 { + return x + } + return Fibonacci(x-1).(int) + Fibonacci(x-2).(int) + }) + + decimals := []int{1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1} + romans := []string{"M", "CM", "D", "CD", "C", "XC", "L", "XL", "X", + "IX", "V", "IV", "I"} + RomanForDecimal = Memoize(func(x int, xs ...int) interface{} { + if x < 0 || x > 3999 { + panic("RomanForDecimal() only handles integers [0, 3999]") + } + var buffer bytes.Buffer + for i, decimal := range decimals { + remainder := x / decimal + x %= decimal + if remainder > 0 { + buffer.WriteString(strings.Repeat(romans[i], remainder)) + } + } + return buffer.String() + }) +} + +func main() { + fmt.Println("Fibonacci(45) =", Fibonacci(45).(int)) + for _, x := range []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, + 14, 15, 16, 17, 18, 19, 20, 25, 30, 40, 50, 60, 69, 70, 80, + 90, 99, 100, 200, 300, 400, 500, 600, 666, 700, 800, 900, + 1000, 1009, 1444, 1666, 1945, 1997, 1999, 2000, 2008, 2010, + 2012, 2500, 3000, 3999} { + fmt.Printf("%4d = %s\n", x, RomanForDecimal(x).(string)) + } +} + +func Memoize(function memoizeFunction) memoizeFunction { + cache := make(map[string]interface{}) + return func(x int, xs ...int) interface{} { + key := fmt.Sprint(x) + for _, i := range xs { + key += fmt.Sprintf(",%d", i) + } + if value, found := cache[key]; found { + return value + } + value := function(x, xs...) + cache[key] = value + return value + } +} diff --git a/src/ordered_map/ordered_map.go b/src/ordered_map/ordered_map.go new file mode 100644 index 0000000..5142e1d --- /dev/null +++ b/src/ordered_map/ordered_map.go @@ -0,0 +1,103 @@ +// Copyright © 2011-12 Qtrac Ltd. +// +// This program or package and any associated files are licensed under the +// Apache License, Version 2.0 (the "License"); you may not use these files +// except in compliance with the License. You can get a copy of the License +// at: http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "fmt" + "math" + "qtrac.eu/omap" + "strings" +) + +type Point struct{ X, Y int } + +func (point Point) String() string { + return fmt.Sprintf("(%d, %d)", point.X, point.Y) +} + +func main() { + words := []string{"Puttering", "About", "in", "a", "Small", "Land"} + wordForWord := omap.NewCaseFoldedKeyed() + fmt.Println(wordForWord.Len(), "words") + for _, word := range words { + wordForWord.Insert(word, strings.ToUpper(word)) + } + wordForWord.Do(func(key, value interface{}) { + fmt.Printf("%v→%v\n", key, value) + }) + fmt.Println("length before deleting:", wordForWord.Len()) + _, containsSmall := wordForWord.Find("small") + fmt.Println("contains small:", containsSmall) + for _, key := range []string{"big", "medium", "small"} { + fmt.Printf("%t ", wordForWord.Delete(key)) + } + _, containsSmall = wordForWord.Find("small") + fmt.Println("\nlength after deleting: ", wordForWord.Len()) + fmt.Println("contains small:", containsSmall) + + showMap(wordForWord, words, "words", 20) + searchMap(wordForWord, "small", "big") + + fmt.Println() + + distanceForPoint := omap.New(func(a, b interface{}) bool { + α, β := a.(*Point), b.(*Point) + if α.X != β.X { + return α.X < β.X + } + return α.Y < β.Y + }) + fmt.Println(distanceForPoint.Len(), "points") + points := []*Point{{3, 1}, {1, 2}, {2, 3}, {1, 3}, {3, 2}, {2, 1}, {2, 2}} + for _, point := range points { + distance := math.Hypot(float64(point.X), float64(point.Y)) + distanceForPoint.Insert(point, distance) + } + distanceForPoint.Do(func(key, value interface{}) { + fmt.Printf("%v → %.2v\n", key, value) + }) + // No &distanceForPoint because it is already a pointer + showMap(distanceForPoint, points, "points", 5) + searchMap(distanceForPoint, &Point{1, 1}, &Point{3, 2}) +} + +func showMap(omap *omap.Map, data interface{}, name string, + width int) { + fmt.Println("original: ", data) + fmt.Print("omap keys: [") + gap := "" + omap.Do(func(key, _ interface{}) { + fmt.Print(gap, key) + gap = " " + }) + fmt.Println("]") + fmt.Print("omap values: [") + gap = "" + omap.Do(func(_, value interface{}) { + fmt.Printf("%s%.*v", gap, width, value) + gap = " " + }) + fmt.Println("]") + fmt.Println(omap.Len(), name) +} + +func searchMap(omap *omap.Map, keys ...interface{}) { + for _, key := range keys { + if value, found := omap.Find(key); found { + fmt.Printf("\"%v\" is in the omap with value %v\n", key, value) + } else { + fmt.Printf("\"%v\" isn't in the omap\n", key) + } + } +} diff --git a/src/oslice/oslice.go b/src/oslice/oslice.go new file mode 100644 index 0000000..5f5ede0 --- /dev/null +++ b/src/oslice/oslice.go @@ -0,0 +1,108 @@ +// Copyright © 2011-12 Qtrac Ltd. +// +// This program or package and any associated files are licensed under the +// Apache License, Version 2.0 (the "License"); you may not use these files +// except in compliance with the License. You can get a copy of the License +// at: http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package oslice + +import "strings" + +func New(less func(interface{}, interface{}) bool) *Slice { + return &Slice{less: less} +} + +func NewStringSlice() *Slice { + return &Slice{less: func(a, b interface{}) bool { + return a.(string) < b.(string) + }} +} + +func NewCaseFoldedSlice() *Slice { + return &Slice{less: func(a, b interface{}) bool { + return strings.ToLower(a.(string)) < strings.ToLower(b.(string)) + }} +} + +func NewIntSlice() *Slice { + return &Slice{less: func(a, b interface{}) bool { + return a.(int) < b.(int) + }} +} + +type Slice struct { + slice []interface{} + less func(interface{}, interface{}) bool +} + +func (slice *Slice) Clear() { + slice.slice = nil +} + +func (slice *Slice) Add(x interface{}) { + if slice.slice == nil { + slice.slice = []interface{}{x} + } else if index := bisectLeft(slice.slice, slice.less, x); + index == len(slice.slice) { + slice.slice = append(slice.slice, x) + } else { // See Chapter 4's InsertStringSlice for the logic + updatedSlice := make([]interface{}, len(slice.slice)+1) + at := copy(updatedSlice, slice.slice[:index]) + at += copy(updatedSlice[at:], []interface{}{x}) + copy(updatedSlice[at:], slice.slice[index:]) + slice.slice = updatedSlice + } +} + +func (slice *Slice) Remove(x interface{}) bool { + index := bisectLeft(slice.slice, slice.less, x) + for ; index < len(slice.slice); index++ { + if !slice.less(slice.slice[index], x) && + !slice.less(x, slice.slice[index]) { + slice.slice = append(slice.slice[:index], + slice.slice[index+1:]...) + return true + } + } + return false +} + +func (slice *Slice) Index(x interface{}) int { + index := bisectLeft(slice.slice, slice.less, x) + if index >= len(slice.slice) || + slice.less(slice.slice[index], x) || + slice.less(x, slice.slice[index]) { + return -1 + } + return index +} + +func (slice *Slice) At(index int) interface{} { + return slice.slice[index] +} + +func (slice *Slice) Len() int { + return len(slice.slice) +} + +// Return's the index position where x belongs in the slice +func bisectLeft(slice []interface{}, + less func(interface{}, interface{}) bool, x interface{}) int { + left, right := 0, len(slice) + for left < right { + middle := int((left + right) / 2) + if less(slice[middle], x) { + left = middle + 1 + } else { + right = middle + } + } + return left +} diff --git a/src/oslice/oslice_test.go b/src/oslice/oslice_test.go new file mode 100644 index 0000000..a94f705 --- /dev/null +++ b/src/oslice/oslice_test.go @@ -0,0 +1,91 @@ +// Copyright © 2011-12 Qtrac Ltd. +// +// This program or package and any associated files are licensed under the +// Apache License, Version 2.0 (the "License"); you may not use these files +// except in compliance with the License. You can get a copy of the License +// at: http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package oslice_test + +import ( + "fmt" + "oslice" + "reflect" + "testing" +) + +func TestSortedIntList(t *testing.T) { + slice := oslice.NewIntSlice() + for _, x := range []int{5, 8, -1, 3, 4, 22} { + slice.Add(x) + } + checkList(slice, []int{-1, 3, 4, 5, 8, 22}, t) + for _, x := range []int{5, 5, 6} { + slice.Add(x) + } + checkList(slice, []int{-1, 3, 4, 5, 5, 5, 6, 8, 22}, t) + if slice.Index(4) != 2 { + t.Fatal("4 missing") + } + printSlice(slice) + if slice.Index(99) != -1 { + t.Fatal("99 wrongly present") + } + if slice.Remove(99) != false { + t.Fatal("99 wrongly removed") + } + if slice.Remove(5) != true { + t.Fatal("5 not removed") + } + checkList(slice, []int{-1, 3, 4, 5, 5, 6, 8, 22}, t) + if slice.Remove(5) != true { + t.Fatal("5 not removed") + } + checkList(slice, []int{-1, 3, 4, 5, 6, 8, 22}, t) + if slice.Remove(5) != true { + t.Fatal("5 not removed") + } + checkList(slice, []int{-1, 3, 4, 6, 8, 22}, t) + if slice.Index(5) != -1 { + t.Fatalf("5 wrongly present at %d", slice.Index(5)) + } + printSlice(slice) + slice.Clear() + if slice.Len() != 0 { + t.Fatal("cleared list isn't empty") + } + if slice.Remove(9) != false { + t.Fatal("9 wrongly removed") + } + if slice.Index(9) != -1 { + t.Fatal("9 wrongly found") + } +} + +func printSlice(slice *oslice.Slice) { + fmt.Print("[") + sep := ", " + for i := 0; i < slice.Len(); i++ { + if i+1 == slice.Len() { + sep = "]\n" + } + fmt.Print(slice.At(i), sep) + } +} + +func checkList(slice *oslice.Slice, ints []int, t *testing.T) { + if slice.Len() != len(ints) { + t.Fatalf("slice.Len()=%d != %d", slice.Len(), len(ints)) + } + for i := 0; i < slice.Len(); i++ { + if !reflect.DeepEqual(slice.At(i), ints[i]) { + t.Fatalf("mismtach At(%d) %v vs. %d", i, slice.At(i), ints[i]) + } + } +} diff --git a/src/pack/pack.go b/src/pack/pack.go new file mode 100644 index 0000000..856e0c4 --- /dev/null +++ b/src/pack/pack.go @@ -0,0 +1,171 @@ +// Copyright © 2011-12 Qtrac Ltd. +// +// This program or package and any associated files are licensed under the +// Apache License, Version 2.0 (the "License"); you may not use these files +// except in compliance with the License. You can get a copy of the License +// at: http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "archive/tar" + "archive/zip" + "compress/gzip" + "fmt" + "io" + "log" + "os" + "path/filepath" + "runtime" + "strings" +) + +func main() { + log.SetFlags(0) + if len(os.Args) < 3 || os.Args[1] == "-h" || os.Args[1] == "--help" { + fmt.Printf("usage: %s archive.{zip,tar,tar.gz} "+ + "file1 [file2 [... fileN]]\n", filepath.Base(os.Args[0])) + os.Exit(1) + + } + filename := os.Args[1] + if !validSuffix(filename) { + log.Fatalln("unrecognized archive suffix") + } + files := commandLineFiles(os.Args[2:]) + if err := createArchive(filename, files); err != nil { + log.Fatalln(err) + } +} + +func validSuffix(filename string) bool { + for _, suffix := range []string{".zip", ".tar", ".tar.gz"} { + if strings.HasSuffix(filename, suffix) { + return true + } + } + return false +} + +func commandLineFiles(files []string) []string { + if runtime.GOOS == "windows" { + args := make([]string, 0, len(files)) + for _, name := range files { + if matches, err := filepath.Glob(name); err != nil { + args = append(args, name) // Invalid pattern + } else if matches != nil { // At least one match + args = append(args, matches...) + } + } + return args + } + return files +} + +func createArchive(filename string, files []string) error { + if strings.HasSuffix(filename, ".zip") { + return createZip(filename, files) + } + return createTar(filename, files) +} + +func createZip(filename string, files []string) error { + file, err := os.Create(filename) + if err != nil { + return err + } + defer file.Close() + zipper := zip.NewWriter(file) + defer zipper.Close() + for _, name := range files { + if err := writeFileToZip(zipper, name); err != nil { + return err + } + } + return nil +} + +func writeFileToZip(zipper *zip.Writer, filename string) error { + file, err := os.Open(filename) + if err != nil { + return err + } + defer file.Close() + info, err := file.Stat() + if err != nil { + return err + } + header, err := zip.FileInfoHeader(info) + if err != nil { + return err + } + header.Name = sanitizedName(filename) + writer, err := zipper.CreateHeader(header) + if err != nil { + return err + } + _, err = io.Copy(writer, file) + return err +} + +func createTar(filename string, files []string) error { + file, err := os.Create(filename) + if err != nil { + return err + } + defer file.Close() + var fileWriter io.WriteCloser = file + if strings.HasSuffix(filename, ".gz") { + fileWriter = gzip.NewWriter(file) + defer fileWriter.Close() + } + writer := tar.NewWriter(fileWriter) + defer writer.Close() + for _, name := range files { + if err := writeFileToTar(writer, name); err != nil { + return err + } + } + return nil +} + +func writeFileToTar(writer *tar.Writer, filename string) error { + file, err := os.Open(filename) + if err != nil { + return err + } + defer file.Close() + stat, err := file.Stat() + if err != nil { + return err + } + header := &tar.Header{ + Name: sanitizedName(filename), + Mode: int64(stat.Mode()), + Uid: os.Getuid(), + Gid: os.Getgid(), + Size: stat.Size(), + ModTime: stat.ModTime(), + } + if err = writer.WriteHeader(header); err != nil { + return err + } + _, err = io.Copy(writer, file) + return err +} + +func sanitizedName(filename string) string { + if len(filename) > 1 && filename[1] == ':' && + runtime.GOOS == "windows" { + filename = filename[2:] + } + filename = filepath.ToSlash(filename) + filename = strings.TrimLeft(filename, "/.") + return strings.Replace(filename, "../", "", -1) +} diff --git a/src/palindrome/palindrome.go b/src/palindrome/palindrome.go new file mode 100644 index 0000000..6797492 --- /dev/null +++ b/src/palindrome/palindrome.go @@ -0,0 +1,64 @@ +// Copyright © 2011-12 Qtrac Ltd. +// +// This program or package and any associated files are licensed under the +// Apache License, Version 2.0 (the "License"); you may not use these files +// except in compliance with the License. You can get a copy of the License +// at: http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "fmt" + "os" + "path/filepath" + "unicode/utf8" +) + +var IsPalindrome func(string) bool // Holds a reference to a function + + +func init() { + if len(os.Args) > 1 && + (os.Args[1] == "-a" || os.Args[1] == "--ascii") { + os.Args = append(os.Args[:1], os.Args[2:]...) // Strip out arg. + IsPalindrome = func(s string) bool { // Simple ASCII-only version + if len(s) <= 1 { + return true + } + if s[0] != s[len(s)-1] { + return false + } + return IsPalindrome(s[1 : len(s)-1]) + } + } else { + IsPalindrome = func(s string) bool { // UTF-8 version + if utf8.RuneCountInString(s) <= 1 { + return true + } + first, sizeOfFirst := utf8.DecodeRuneInString(s) + last, sizeOfLast := utf8.DecodeLastRuneInString(s) + if first != last { + return false + } + return IsPalindrome(s[sizeOfFirst : len(s)-sizeOfLast]) + } + } +} + +func main() { + if len(os.Args) == 1 { + fmt.Printf("usage: %s [-a|--ascii] word1 [word2 [... wordN]]\n", + filepath.Base(os.Args[0])) + os.Exit(1) + } + words := os.Args[1:] + for _, word := range words { + fmt.Printf("%5t %q\n", IsPalindrome(word), word) + } +} diff --git a/src/palindrome_ans/palindrome.go b/src/palindrome_ans/palindrome.go new file mode 100644 index 0000000..9d74f00 --- /dev/null +++ b/src/palindrome_ans/palindrome.go @@ -0,0 +1,67 @@ +// Copyright © 2011-12 Qtrac Ltd. +// +// This program or package and any associated files are licensed under the +// Apache License, Version 2.0 (the "License"); you may not use these files +// except in compliance with the License. You can get a copy of the License +// at: http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "fmt" + "os" + "path/filepath" + "unicode/utf8" +) + +var IsPalindrome func(string) bool + +func init() { + if len(os.Args) > 1 && + (os.Args[1] == "-a" || os.Args[1] == "--ascii") { + os.Args = append(os.Args[:1], os.Args[2:]...) // Strip out arg. + IsPalindrome = func(s string) bool { // Simple ASCII-only version + j := len(s) - 1 + for i := 0; i < len(s)/2; i++ { + if s[i] != s[j] { + return false + } + j-- + } + return true + } + } else { + IsPalindrome = func(s string) bool { // UTF-8 version + for len(s) > 0 { + first, sizeOfFirst := utf8.DecodeRuneInString(s) + if sizeOfFirst == len(s) { + break // s only has one character + } + last, sizeOfLast := utf8.DecodeLastRuneInString(s) + if first != last { + return false + } + s = s[sizeOfFirst : len(s)-sizeOfLast] + } + return true + } + } +} + +func main() { + if len(os.Args) == 1 { + fmt.Printf("usage: %s [-a|--ascii] word1 [word2 [... wordN]]\n", + filepath.Base(os.Args[0])) + os.Exit(1) + } + words := os.Args[1:] + for _, word := range words { + fmt.Printf("%5t %q\n", IsPalindrome(word), word) + } +} diff --git a/src/pi_by_digits/pi_by_digits.go b/src/pi_by_digits/pi_by_digits.go new file mode 100644 index 0000000..22eac8f --- /dev/null +++ b/src/pi_by_digits/pi_by_digits.go @@ -0,0 +1,89 @@ +// Copyright © 2011-12 Qtrac Ltd. +// +// This program or package and any associated files are licensed under the +// Apache License, Version 2.0 (the "License"); you may not use these files +// except in compliance with the License. You can get a copy of the License +// at: http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Algorithm taken from: +// http://en.literateprograms.org/Pi_with_Machin%27s_formula_%28Python%29 + +package main + +import ( + "fmt" + "math/big" + "os" + "path/filepath" + "strconv" +) + +func main() { + places := handleCommandLine(1000) + scaledPi := fmt.Sprint(π(places)) + fmt.Printf("3.%s\n", scaledPi[1:]) +} + +func handleCommandLine(defaultValue int) int { + if len(os.Args) > 1 { + if os.Args[1] == "-h" || os.Args[1] == "--help" { + usage := "usage: %s [digits]\n e.g.: %s 10000" + app := filepath.Base(os.Args[0]) + fmt.Fprintln(os.Stderr, fmt.Sprintf(usage, app, app)) + os.Exit(1) + } + if x, err := strconv.Atoi(os.Args[1]); err != nil { + fmt.Fprintf(os.Stderr, "ignoring invalid number of "+ + "digits: will display %d\n", defaultValue) + } else { + return x + } + } + return defaultValue +} + +func π(places int) *big.Int { + digits := big.NewInt(int64(places)) + unity := big.NewInt(0) + ten := big.NewInt(10) + exponent := big.NewInt(0) + unity.Exp(ten, exponent.Add(digits, ten), nil) + pi := big.NewInt(4) + left := arccot(big.NewInt(5), unity) + left.Mul(left, big.NewInt(4)) + right := arccot(big.NewInt(239), unity) + left.Sub(left, right) + pi.Mul(pi, left) + return pi.Div(pi, big.NewInt(0).Exp(ten, ten, nil)) +} + +func arccot(x, unity *big.Int) *big.Int { + sum := big.NewInt(0) + sum.Div(unity, x) + xpower := big.NewInt(0) + xpower.Div(unity, x) + n := big.NewInt(3) + sign := big.NewInt(-1) + zero := big.NewInt(0) + square := big.NewInt(0) + square.Mul(x, x) + for { + xpower.Div(xpower, square) + term := big.NewInt(0) + term.Div(xpower, n) + if term.Cmp(zero) == 0 { + break + } + addend := big.NewInt(0) + sum.Add(sum, addend.Mul(sign, term)) + sign.Neg(sign) + n.Add(n, big.NewInt(2)) + } + return sum +} diff --git a/src/playlist/David-Bowie-Singles.m3u b/src/playlist/David-Bowie-Singles.m3u new file mode 100644 index 0000000..96e260a --- /dev/null +++ b/src/playlist/David-Bowie-Singles.m3u @@ -0,0 +1,67 @@ +#EXTM3U +#EXTINF:315,David Bowie - Space Oddity +Music/David Bowie/Singles 1/01-Space Oddity.ogg +#EXTINF:-1,David Bowie - Changes +Music/David Bowie/Singles 1/02-Changes.ogg +#EXTINF:258,David Bowie - Starman +Music/David Bowie/Singles 1/03-Starman.ogg +#EXTINF:194,David Bowie - Ziggy Stardust +Music/David Bowie/Singles 1/04-Ziggy Stardust.ogg +#EXTINF:206,David Bowie - Suffragette City +Music/David Bowie/Singles 1/05-Suffragette City.ogg +#EXTINF:247,David Bowie - The Jean Genie +Music/David Bowie/Singles 1/07-The Jean Genie.ogg +#EXTINF:231,David Bowie - Life On Mars? +Music/David Bowie/Singles 1/09-Life On Mars.ogg +#EXTINF:174,David Bowie - Sorrow +Music/David Bowie/Singles 1/10-Sorrow.ogg +#EXTINF:270,David Bowie - Rebel Rebel +Music/David Bowie/Singles 1/11-Rebel Rebel.ogg +#EXTINF:178,David Bowie - Rock & Roll Suicide +Music/David Bowie/Singles 1/12-Rock and Roll Suicide.ogg +#EXTINF:364,David Bowie - Diamond Dogs +Music/David Bowie/Singles 1/13-Diamond Dogs.ogg +#EXTINF:311,David Bowie - Young Americans +Music/David Bowie/Singles 1/15-Young Americans.ogg +#EXTINF:254,David Bowie - Fame +Music/David Bowie/Singles 1/16-Fame.ogg +#EXTINF:240,David Bowie - Golden Years +Music/David Bowie/Singles 1/17-Golden Years.ogg +#EXTINF:331,David Bowie - TVC 15 +Music/David Bowie/Singles 1/18-TVC 15.ogg +#EXTINF:183,David Bowie - Sound & Vision +Music/David Bowie/Singles 1/19-Sound & Vision.ogg +#EXTINF:217,David Bowie - Heroes +Music/David Bowie/Singles 2/01-Heroes.ogg +#EXTINF:214,David Bowie - Beauty & The Beast +Music/David Bowie/Singles 2/02-Beauty and The Beast.ogg +#EXTINF:197,David Bowie - Boys Keep Swinging +Music/David Bowie/Singles 2/03-Boys Keep Swinging.ogg +#EXTINF:240,David Bowie - D.J. +Music/David Bowie/Singles 2/04-D.J..ogg +#EXTINF:-1,David Bowie - Alabama Song +Music/David Bowie/Singles 2/05-Alabama Song.ogg +#EXTINF:264,David Bowie - Ashes To Ashes +Music/David Bowie/Singles 2/06-Ashes To Ashes.ogg +#EXTINF:287,David Bowie - Fashion +Music/David Bowie/Singles 2/07-Fashion.ogg +#EXTINF:311,David Bowie - Scary Monsters (And Super Creeps) +Music/David Bowie/Singles 2/08-Scary Monsters (And Super Creeps).ogg +#EXTINF:237,David Bowie - Under Pressure +Music/David Bowie/Singles 2/09-Under Pressure.ogg +#EXTINF:361,David Bowie - Wild Is The Wind +Music/David Bowie/Singles 2/10-Wild Is The Wind.ogg +#EXTINF:250,David Bowie - Let's Dance +Music/David Bowie/Singles 2/11-Let's Dance.ogg +#EXTINF:256,David Bowie - China Girl +Music/David Bowie/Singles 2/12-China Girl.ogg +#EXTINF:237,David Bowie - Modern Love +Music/David Bowie/Singles 2/13-Modern Love.ogg +#EXTINF:191,David Bowie - Blue Jean +Music/David Bowie/Singles 2/14-Blue Jean.ogg +#EXTINF:230,David Bowie - This Is Not America +Music/David Bowie/Singles 2/15-This Is Not America.ogg +#EXTINF:337,David Bowie - Absolute Beginners +Music/David Bowie/Singles 2/17-Absolute Beginners.ogg +#EXTINF:251,David Bowie - Day In Day Out +Music/David Bowie/Singles 2/18-Day In Day Out.ogg diff --git a/src/playlist/David-Bowie-Singles.pls b/src/playlist/David-Bowie-Singles.pls new file mode 100644 index 0000000..0dcebfd --- /dev/null +++ b/src/playlist/David-Bowie-Singles.pls @@ -0,0 +1,102 @@ +[playlist] +File1=Music/David Bowie/Singles 1/01-Space Oddity.ogg +Title1=David Bowie - Space Oddity +Length1=315 +File2=Music/David Bowie/Singles 1/02-Changes.ogg +Title2=David Bowie - Changes +Length2=-1 +File3=Music/David Bowie/Singles 1/03-Starman.ogg +Title3=David Bowie - Starman +Length3=258 +File4=Music/David Bowie/Singles 1/04-Ziggy Stardust.ogg +Title4=David Bowie - Ziggy Stardust +Length4=194 +File5=Music/David Bowie/Singles 1/05-Suffragette City.ogg +Title5=David Bowie - Suffragette City +Length5=206 +File6=Music/David Bowie/Singles 1/07-The Jean Genie.ogg +Title6=David Bowie - The Jean Genie +Length6=247 +File7=Music/David Bowie/Singles 1/09-Life On Mars.ogg +Title7=David Bowie - Life On Mars? +Length7=231 +File8=Music/David Bowie/Singles 1/10-Sorrow.ogg +Title8=David Bowie - Sorrow +Length8=174 +File9=Music/David Bowie/Singles 1/11-Rebel Rebel.ogg +Title9=David Bowie - Rebel Rebel +Length9=270 +File10=Music/David Bowie/Singles 1/12-Rock and Roll Suicide.ogg +Title10=David Bowie - Rock & Roll Suicide +Length10=178 +File11=Music/David Bowie/Singles 1/13-Diamond Dogs.ogg +Title11=David Bowie - Diamond Dogs +Length11=364 +File12=Music/David Bowie/Singles 1/15-Young Americans.ogg +Title12=David Bowie - Young Americans +Length12=311 +File13=Music/David Bowie/Singles 1/16-Fame.ogg +Title13=David Bowie - Fame +Length13=254 +File14=Music/David Bowie/Singles 1/17-Golden Years.ogg +Title14=David Bowie - Golden Years +Length14=240 +File15=Music/David Bowie/Singles 1/18-TVC 15.ogg +Title15=David Bowie - TVC 15 +Length15=331 +File16=Music/David Bowie/Singles 1/19-Sound & Vision.ogg +Title16=David Bowie - Sound & Vision +Length16=183 +File17=Music/David Bowie/Singles 2/01-Heroes.ogg +Title17=David Bowie - Heroes +Length17=217 +File18=Music/David Bowie/Singles 2/02-Beauty and The Beast.ogg +Title18=David Bowie - Beauty & The Beast +Length18=214 +File19=Music/David Bowie/Singles 2/03-Boys Keep Swinging.ogg +Title19=David Bowie - Boys Keep Swinging +Length19=197 +File20=Music/David Bowie/Singles 2/04-D.J..ogg +Title20=David Bowie - D.J. +Length20=240 +File21=Music/David Bowie/Singles 2/05-Alabama Song.ogg +Title21=David Bowie - Alabama Song +Length21=-1 +File22=Music/David Bowie/Singles 2/06-Ashes To Ashes.ogg +Title22=David Bowie - Ashes To Ashes +Length22=264 +File23=Music/David Bowie/Singles 2/07-Fashion.ogg +Title23=David Bowie - Fashion +Length23=287 +File24=Music/David Bowie/Singles 2/08-Scary Monsters (And Super Creeps).ogg +Title24=David Bowie - Scary Monsters (And Super Creeps) +Length24=311 +File25=Music/David Bowie/Singles 2/09-Under Pressure.ogg +Title25=David Bowie - Under Pressure +Length25=237 +File26=Music/David Bowie/Singles 2/10-Wild Is The Wind.ogg +Title26=David Bowie - Wild Is The Wind +Length26=361 +File27=Music/David Bowie/Singles 2/11-Let's Dance.ogg +Title27=David Bowie - Let's Dance +Length27=250 +File28=Music/David Bowie/Singles 2/12-China Girl.ogg +Title28=David Bowie - China Girl +Length28=256 +File29=Music/David Bowie/Singles 2/13-Modern Love.ogg +Title29=David Bowie - Modern Love +Length29=237 +File30=Music/David Bowie/Singles 2/14-Blue Jean.ogg +Title30=David Bowie - Blue Jean +Length30=191 +File31=Music/David Bowie/Singles 2/15-This Is Not America.ogg +Title31=David Bowie - This Is Not America +Length31=230 +File32=Music/David Bowie/Singles 2/17-Absolute Beginners.ogg +Title32=David Bowie - Absolute Beginners +Length32=337 +File33=Music/David Bowie/Singles 2/18-Day In Day Out.ogg +Title33=David Bowie - Day In Day Out +Length33=251 +NumberOfEntries=33 +Version=2 diff --git a/src/playlist/playlist.go b/src/playlist/playlist.go new file mode 100644 index 0000000..95ebb52 --- /dev/null +++ b/src/playlist/playlist.go @@ -0,0 +1,155 @@ +// Copyright © 2011-12 Qtrac Ltd. +// +// This program or package and any associated files are licensed under the +// Apache License, Version 2.0 (the "License"); you may not use these files +// except in compliance with the License. You can get a copy of the License +// at: http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "fmt" + "io/ioutil" + "log" + "os" + "path/filepath" + "strconv" + "strings" +) + +type Song struct { + Title string + Filename string + Seconds int +} + +func main() { + if len(os.Args) == 1 || + (!strings.HasSuffix(os.Args[1], ".pls") && + !strings.HasSuffix(os.Args[1], ".m3u")) { + fmt.Printf("usage: %s \n", + filepath.Base(os.Args[0])) + os.Exit(1) + } + + if rawBytes, err := ioutil.ReadFile(os.Args[1]); err != nil { + log.Fatal(err) + } else { + data := string(rawBytes) + if strings.HasSuffix(os.Args[1], ".pls") { + songs := readPlsPlaylist(data) + writeM3uPlaylist(songs) + } else { + songs := readM3uPlaylist(data) + writePlsPlaylist(songs) + } + } +} + +func readPlsPlaylist(data string) (songs []Song) { + var song Song + for _, line := range strings.Split(data, "\n") { + if line = strings.TrimSpace(line); line == "" { + continue + } + switch name, value := parsePlsLine(line); name { + case "File": + song.Filename = strings.Map( + mapPlatformDirSeparator, value) + case "Title": + song.Title = value + case "Length": + var err error + if song.Seconds, err = strconv.Atoi(value); err != nil { + log.Printf("failed to read the duration for '%s': %v\n", + song.Title, err) + song.Seconds = -1 + } + } + if song.Filename != "" && song.Title != "" && song.Seconds != 0 { + songs = append(songs, song) + song = Song{} + } + } + return songs +} + +func parsePlsLine(line string) (name, value string) { + const separator = "=" + if i := strings.Index(line, separator); i > -1 { + if j := strings.IndexAny(line, "0123456789"); j > -1 && j < i { + name = line[:j] + value = line[i+len(separator):] + } + } + return name, value +} + +func mapPlatformDirSeparator(char rune) rune { + if char == '/' || char == '\\' { + return filepath.Separator + } + return char +} + +func writePlsPlaylist(songs []Song) { + fmt.Println("[playlist]") + for i, song := range songs { + i++ + fmt.Printf("File%d=%s\n", i, song.Filename) + fmt.Printf("Title%d=%s\n", i, song.Title) + fmt.Printf("Length%d=%d\n", i, song.Seconds) + } + fmt.Printf("NumberOfEntries=%d\nVersion=2\n", len(songs)) +} + +func readM3uPlaylist(data string) (songs []Song) { + var song Song + for _, line := range strings.Split(data, "\n") { + line = strings.TrimSpace(line) + if line == "" || strings.HasPrefix(line, "#EXTM3U") { + continue + } + if strings.HasPrefix(line, "#EXTINF:") { + song.Title, song.Seconds = parseExtinfLine(line) + } else { + song.Filename = strings.Map(mapPlatformDirSeparator, line) + } + if song.Filename != "" && song.Title != "" && song.Seconds != 0 { + songs = append(songs, song) + song = Song{} + } + } + return songs +} + +func parseExtinfLine(line string) (title string, seconds int) { + if i := strings.IndexAny(line, "-0123456789"); i > -1 { + const separator = "," + line = line[i:] + if j := strings.Index(line, separator); j > -1 { + title = line[j+len(separator):] + var err error + if seconds, err = strconv.Atoi(line[:j]); err != nil { + log.Printf("failed to read the duration for '%s': %v\n", + title, err) + seconds = -1 + } + } + } + return title, seconds +} + +func writeM3uPlaylist(songs []Song) { + fmt.Println("#EXTM3U") + for _, song := range songs { + fmt.Printf("#EXTINF:%d,%s\n", song.Seconds, song.Title) + fmt.Println(song.Filename) + } +} diff --git a/src/polar2cartesian/polar2cartesian.go b/src/polar2cartesian/polar2cartesian.go new file mode 100644 index 0000000..7dffd39 --- /dev/null +++ b/src/polar2cartesian/polar2cartesian.go @@ -0,0 +1,88 @@ +// Copyright © 2010-12 Qtrac Ltd. +// +// This program or package and any associated files are licensed under the +// Apache License, Version 2.0 (the "License"); you may not use these files +// except in compliance with the License. You can get a copy of the License +// at: http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "bufio" + "fmt" + "math" + "os" + "runtime" +) + +const result = "Polar radius=%.02f θ=%.02f° → Cartesian x=%.02f y=%.02f\n" + +var prompt = "Enter a radius and an angle (in degrees), e.g., 12.5 90, " + + "or %s to quit." + +type polar struct { + radius float64 + θ float64 +} + +type cartesian struct { + x float64 + y float64 +} + +func init() { + if runtime.GOOS == "windows" { + prompt = fmt.Sprintf(prompt, "Ctrl+Z, Enter") + } else { // Unix-like + prompt = fmt.Sprintf(prompt, "Ctrl+D") + } +} + +func main() { + questions := make(chan polar) + defer close(questions) + answers := createSolver(questions) + defer close(answers) + interact(questions, answers) +} + +func createSolver(questions chan polar) chan cartesian { + answers := make(chan cartesian) + go func() { + for { + polarCoord := <-questions + θ := polarCoord.θ * math.Pi / 180.0 // degrees to radians + x := polarCoord.radius * math.Cos(θ) + y := polarCoord.radius * math.Sin(θ) + answers <- cartesian{x, y} + } + }() + return answers +} + +func interact(questions chan polar, answers chan cartesian) { + reader := bufio.NewReader(os.Stdin) + fmt.Println(prompt) + for { + fmt.Printf("Radius and angle: ") + line, err := reader.ReadString('\n') + if err != nil { + break + } + var radius, θ float64 + if _, err := fmt.Sscanf(line, "%f %f", &radius, &θ); err != nil { + fmt.Fprintln(os.Stderr, "invalid input") + continue + } + questions <- polar{radius, θ} + coord := <-answers + fmt.Printf(result, radius, θ, coord.x, coord.y) + } + fmt.Println() +} diff --git a/src/qtrac.eu/omap/omap.go b/src/qtrac.eu/omap/omap.go new file mode 100644 index 0000000..f3abbf4 --- /dev/null +++ b/src/qtrac.eu/omap/omap.go @@ -0,0 +1,294 @@ +// Copyright © 2011-12 Qtrac Ltd. +// +// This program or package and any associated files are licensed under the +// Apache License, Version 2.0 (the "License"); you may not use these files +// except in compliance with the License. You can get a copy of the License +// at: http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// The data structure is a left-leaning red-black tree based on Robert +// Sedgewick's implementations as described in +// http://www.cs.princeton.edu/~rs/talks/LLRB/LLRB.pdf and +// http://www.cs.princeton.edu/~rs/talks/LLRB/RedBlack.pdf, with some of +// the code based on Lee Stanza's C++ code at +// http://www.teachsolaisgames.com/articles/balanced_left_leaning.html +// Thanks also to Russ Cox for many useful suggestions. + +// Package omap implements an efficient key-ordered map. +// +// Keys and values may be of any type, but all keys must be comparable +// using the less than function that is passed in to the omap.New() +// function, or the less than function provided by the omap.New*() +// construction functions. +package omap + +import "strings" + +// NewStringKeyed returns an empty Map that accepts case-sensitive +// string keys. +func NewStringKeyed() *Map { + return &Map{less: func(a, b interface{}) bool { + return a.(string) < b.(string) + }} +} + +// NewCaseFoldedKeyed returns an empty Map that accepts case-insensitive +// string keys. +func NewCaseFoldedKeyed() *Map { + return &Map{less: func(a, b interface{}) bool { + return strings.ToLower(a.(string)) < strings.ToLower(b.(string)) + }} +} + +// NewIntKeyed returns an empty Map that accepts int keys. +func NewIntKeyed() *Map { + return &Map{less: func(a, b interface{}) bool { + return a.(int) < b.(int) + }} +} + +// NewFloat64Keyed returns an empty Map that accepts float64 keys. +func NewFloat64Keyed() *Map { + return &Map{less: func(a, b interface{}) bool { + return a.(float64) < b.(float64) + }} +} + +// New returns an empty Map that uses the given less than function to +// compare keys. For example: +// type Point { X, Y int } +// pointMap := omap.New(func(a, b interface{}) bool { +// α, β := a.(Point), b.(Point) +// if α.X != β.X { +// return α.X < β.X +// } +// return α.Y < β.Y +// }) +func New(less func(interface{}, interface{}) bool) *Map { + return &Map{less: less} +} + +// Map is a key-ordered map. +// The zero value is an invalid map! Use one of the construction functions +// (e.g., New()), to create a map for a specific key type. +type Map struct { + root *node + less func(interface{}, interface{}) bool + length int +} + +type node struct { + key, value interface{} + red bool + left, right *node +} + +// Insert inserts a new key-value into the Map and returns true; or +// replaces an existing key-value pair's value if the keys are equal and +// returns false. For example: +// inserted := myMap.Insert(key, value). +func (m *Map) Insert(key, value interface{}) (inserted bool) { + m.root, inserted = m.insert(m.root, key, value) + m.root.red = false + if inserted { + m.length++ + } + return inserted +} + +// Find returns the value and true if the key is in the Map or nil and +// false otherwise. For example: +// value, found := myMap.Find(key). +func (m *Map) Find(key interface{}) (value interface{}, found bool) { + root := m.root + for root != nil { + if m.less(key, root.key) { + root = root.left + } else if m.less(root.key, key) { + root = root.right + } else { + return root.value, true + } + } + return nil, false +} + +// Delete deletes the key-value with the given key from the Map and returns +// true, or does nothing and returns false if there is no key-value with +// the given key. For example: +// deleted := myMap.Delete(key). +func (m *Map) Delete(key interface{}) (deleted bool) { + if m.root != nil { + if m.root, deleted = m.remove(m.root, key); m.root != nil { + m.root.red = false + } + } + if deleted { + m.length-- + } + return deleted +} + +// Do calls the given function on every key-value in the Map in order. +func (m *Map) Do(function func(interface{}, interface{})) { + do(m.root, function) +} + +// Len returns the number of key-value pairs in the map. +func (m *Map) Len() int { + return m.length +} + +func (m *Map) insert(root *node, key, value interface{}) (*node, bool) { + inserted := false + if root == nil { // If the key was in the tree it would belong here + return &node{key: key, value: value, red: true}, true + } + if isRed(root.left) && isRed(root.right) { + colorFlip(root) + } + if m.less(key, root.key) { + root.left, inserted = m.insert(root.left, key, value) + } else if m.less(root.key, key) { + root.right, inserted = m.insert(root.right, key, value) + } else { // The key is already in the tree so just replace its value + root.value = value + } + if isRed(root.right) && !isRed(root.left) { + root = rotateLeft(root) + } + if isRed(root.left) && isRed(root.left.left) { + root = rotateRight(root) + } + return root, inserted +} + +func isRed(root *node) bool { return root != nil && root.red } + +func colorFlip(root *node) { + root.red = !root.red + if root.left != nil { + root.left.red = !root.left.red + } + if root.right != nil { + root.right.red = !root.right.red + } +} + +func rotateLeft(root *node) *node { + x := root.right + root.right = x.left + x.left = root + x.red = root.red + root.red = true + return x +} + +func rotateRight(root *node) *node { + x := root.left + root.left = x.right + x.right = root + x.red = root.red + root.red = true + return x +} + +func do(root *node, function func(interface{}, interface{})) { + if root != nil { + do(root.left, function) + function(root.key, root.value) + do(root.right, function) + } +} + +// We do not provide an exported First() method because this is an +// implementation detail. +func first(root *node) *node { + for root.left != nil { + root = root.left + } + return root +} + +func (m *Map) remove(root *node, key interface{}) (*node, bool) { + deleted := false + if m.less(key, root.key) { + if root.left != nil { + if !isRed(root.left) && !isRed(root.left.left) { + root = moveRedLeft(root) + } + root.left, deleted = m.remove(root.left, key) + } + } else { + if isRed(root.left) { + root = rotateRight(root) + } + if !m.less(key, root.key) && !m.less(root.key, key) && + root.right == nil { + return nil, true + } + if root.right != nil { + if !isRed(root.right) && !isRed(root.right.left) { + root = moveRedRight(root) + } + if !m.less(key, root.key) && !m.less(root.key, key) { + smallest := first(root.right) + root.key = smallest.key + root.value = smallest.value + root.right = deleteMinimum(root.right) + deleted = true + } else { + root.right, deleted = m.remove(root.right, key) + } + } + } + return fixUp(root), deleted +} + +func moveRedLeft(root *node) *node { + colorFlip(root) + if root.right != nil && isRed(root.right.left) { + root.right = rotateRight(root.right) + root = rotateLeft(root) + colorFlip(root) + } + return root +} + +func moveRedRight(root *node) *node { + colorFlip(root) + if root.left != nil && isRed(root.left.left) { + root = rotateRight(root) + colorFlip(root) + } + return root +} + +func deleteMinimum(root *node) *node { + if root.left == nil { + return nil + } + if !isRed(root.left) && !isRed(root.left.left) { + root = moveRedLeft(root) + } + root.left = deleteMinimum(root.left) + return fixUp(root) +} + +func fixUp(root *node) *node { + if isRed(root.right) { + root = rotateLeft(root) + } + if isRed(root.left) && isRed(root.left.left) { + root = rotateRight(root) + } + if isRed(root.left) && isRed(root.right) { + colorFlip(root) + } + return root +} diff --git a/src/qtrac.eu/omap/omap_test.go b/src/qtrac.eu/omap/omap_test.go new file mode 100644 index 0000000..0482858 --- /dev/null +++ b/src/qtrac.eu/omap/omap_test.go @@ -0,0 +1,127 @@ +// Copyright © 2011-12 Qtrac Ltd. +// +// This program or package and any associated files are licensed under the +// Apache License, Version 2.0 (the "License"); you may not use these files +// except in compliance with the License. You can get a copy of the License +// at: http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// The tests here are very incomplete and just to show examples of how it +// can be done. +package omap_test + +import ( + "qtrac.eu/omap" + "strings" + "testing" +) + +func TestStringKeyOMapInsertion(t *testing.T) { + wordForWord := omap.NewCaseFoldedKeyed() + for _, word := range []string{"one", "Two", "THREE", "four", "Five"} { + wordForWord.Insert(word, word) + } + var words []string + wordForWord.Do(func(_, value interface{}) { + words = append(words, value.(string)) + }) + actual, expected := strings.Join(words, ""), "FivefouroneTHREETwo" + if actual != expected { + t.Errorf("%q != %q", actual, expected) + } +} + +func TestIntKeyOMapFind(t *testing.T) { + intMap := omap.NewIntKeyed() + for _, number := range []int{9, 1, 8, 2, 7, 3, 6, 4, 5, 0} { + intMap.Insert(number, number*10) + } + for _, number := range []int{0, 1, 5, 8, 9} { + value, found := intMap.Find(number) + if !found { + t.Errorf("failed to find %d", number) + } + actual, expected := value.(int), number*10 + if actual != expected { + t.Errorf("value is %d should be %d", actual, expected) + } + } + for _, number := range []int{-1, -21, 10, 11, 148} { + _, found := intMap.Find(number) + if found { + t.Errorf("should not have found %d", number) + } + } +} + +func TestIntKeyOMapDelete(t *testing.T) { + intMap := omap.NewIntKeyed() + for _, number := range []int{9, 1, 8, 2, 7, 3, 6, 4, 5, 0} { + intMap.Insert(number, number*10) + } + if intMap.Len() != 10 { + t.Errorf("map len %d should be 10", intMap.Len()) + } + length := 9 + for i, number := range []int{0, 1, 5, 8, 9} { + if deleted := intMap.Delete(number); !deleted { + t.Errorf("failed to delete %d", number) + } + if intMap.Len() != length-i { + t.Errorf("map len %d should be %d", intMap.Len(), length-i) + } + } + for _, number := range []int{-1, -21, 10, 11, 148} { + if deleted := intMap.Delete(number); deleted { + t.Errorf("should not have deleted nonexistent %d", number) + } + } + if intMap.Len() != 5 { + t.Errorf("map len %d should be 5", intMap.Len()) + } +} + +func TestPassing(t *testing.T) { + intMap := omap.NewIntKeyed() + intMap.Insert(7, 7) + passMap(intMap, t) +} + +func passMap(m *omap.Map, t *testing.T) { + for _, number := range []int{9, 3, 6, 4, 5, 0} { + m.Insert(number, number) + } + if m.Len() != 7 { + t.Errorf("should have %d items", 7) + } +} + +// Thanks to Russ Cox for improving these benchmarks +func BenchmarkOMapFindSuccess(b *testing.B) { + b.StopTimer() // Don't time creation and population + intMap := omap.NewIntKeyed() + for i := 0; i < 1e6; i++ { + intMap.Insert(i, i) + } + b.StartTimer() // Time the Find() method succeeding + for i := 0; i < b.N; i++ { + intMap.Find(i % 1e6) + } +} + +func BenchmarkOMapFindFailure(b *testing.B) { + b.StopTimer() // Don't time creation and population + intMap := omap.NewIntKeyed() + for i := 0; i < 1e6; i++ { + intMap.Insert(2*i, i) + } + b.StartTimer() // Time the Find() method failing + for i := 0; i < b.N; i++ { + intMap.Find(2*(i%1e6) + 1) + } +} diff --git a/src/quadratic_ans1/quadratic.go b/src/quadratic_ans1/quadratic.go new file mode 100644 index 0000000..208ceb4 --- /dev/null +++ b/src/quadratic_ans1/quadratic.go @@ -0,0 +1,134 @@ +// Copyright © 2010-12 Qtrac Ltd. +// +// This program or package and any associated files are licensed under the +// Apache License, Version 2.0 (the "License"); you may not use these files +// except in compliance with the License. You can get a copy of the License +// at: http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "fmt" + "log" + "math" + "math/cmplx" + "net/http" + "strconv" +) + +const ( + pageTop = ` + +Quadratic Equation Solver +

Quadratic Equation Solver

Solves equations of the form +ax² + bx + c

` + form = `
+ + + + + + +
` + pageBottom = "" + error = `

%s

` + solution = "

%s → %s

" +) + +func main() { + http.HandleFunc("/", homePage) + if err := http.ListenAndServe(":9001", nil); err != nil { + log.Fatal("failed to start server", err) + } +} + +func homePage(writer http.ResponseWriter, request *http.Request) { + err := request.ParseForm() // Must be called before writing response + fmt.Fprint(writer, pageTop, form) + if err != nil { + fmt.Fprintf(writer, error, err) + } else { + if floats, message, ok := processRequest(request); ok { + question := formatQuestion(request.Form) + x1, x2 := solve(floats) + answer := formatSolutions(x1, x2) + fmt.Fprintf(writer, solution, question, answer) + } else if message != "" { + fmt.Fprintf(writer, error, message) + } + } + fmt.Fprint(writer, pageBottom) +} + +func processRequest(request *http.Request) ([3]float64, string, bool) { + var floats [3]float64 + count := 0 + for index, key := range []string{"a", "b", "c"} { + if slice, found := request.Form[key]; found && len(slice) > 0 { + if slice[0] != "" { + if x, err := strconv.ParseFloat(slice[0], 64); + err != nil { + return floats, "'" + slice[0] + "' is invalid", false + } else { + floats[index] = x + } + } else { // as a courtesy to users treat blanks as 0 + request.Form[key][0] = "0" + floats[index] = 0 + } + count++ + } + } + if count != 3 { // the first time the form is empty; + return floats, "", false // this isn't an error but there's + } // nothing to calculate + if EqualFloat(floats[0], 0, -1) { + return floats, "the x² factor may not be 0", false + } + return floats, "", true +} + +func formatQuestion(form map[string][]string) string { + // Ugly formatting, e.g., 1x² + -1x + -5 should be x² - x - 5, + // 1x² + 0x + -2 should be x² - 2, etc. + return fmt.Sprintf("%sx² + %sx + %s", form["a"][0], + form["b"][0], form["c"][0]) +} + +func formatSolutions(x1, x2 complex128) string { + // Ugly formatting since it always shows a complex even if the imag + // part is 0i. + if EqualComplex(x1, x2) { + return fmt.Sprintf("x=%f", x1) + } + return fmt.Sprintf("x=%f or x=%f", x1, x2) +} + +func solve(floats [3]float64) (complex128, complex128) { + a, b, c := complex(floats[0], 0), complex(floats[1], 0), + complex(floats[2], 0) + root := cmplx.Sqrt(cmplx.Pow(b, 2) - (4 * a * c)) + x1 := (-b + root) / (2 * a) + x2 := (-b - root) / (2 * a) + return x1, x2 +} + +// EqualFloat() returns true if x and y are approximately equal to the +// given limit. Pass a limit of -1 to get the greatest accuracy the machine +// can manage. +func EqualFloat(x, y, limit float64) bool { + if limit <= 0.0 { + limit = math.SmallestNonzeroFloat64 + } + return math.Abs(x-y) <= + (limit * math.Min(math.Abs(x), math.Abs(y))) +} + +func EqualComplex(x, y complex128) bool { + return EqualFloat(real(x), real(y), -1) && + EqualFloat(imag(x), imag(y), -1) +} diff --git a/src/quadratic_ans2/quadratic.go b/src/quadratic_ans2/quadratic.go new file mode 100644 index 0000000..27a0dec --- /dev/null +++ b/src/quadratic_ans2/quadratic.go @@ -0,0 +1,171 @@ +// Copyright © 2010-12 Qtrac Ltd. +// +// This program or package and any associated files are licensed under the +// Apache License, Version 2.0 (the "License"); you may not use these files +// except in compliance with the License. You can get a copy of the License +// at: http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "fmt" + "log" + "math" + "math/cmplx" + "net/http" + "strconv" +) + +const ( + decimals = 3 + pageTop = ` + +Quadratic Equation Solver +

Quadratic Equation Solver

Solves equations of the form +ax² + bx + c

` + form = `
+ + + + + + +
` + pageBottom = "" + error = `

%s

` + solution = "

%s → %s

" + oneSolution = "x=%s" + twoSolutions = "x=%s or x=%s" + noSolution = "there are no solutions" +) + +func main() { + http.HandleFunc("/", homePage) + if err := http.ListenAndServe(":9001", nil); err != nil { + log.Fatal("failed to start server", err) + } +} + +func homePage(writer http.ResponseWriter, request *http.Request) { + err := request.ParseForm() // Must be called before writing response + fmt.Fprint(writer, pageTop, form) + if err != nil { + fmt.Fprintf(writer, error, err) + } else { + if floats, message, ok := processRequest(request); ok { + question := formatQuestion(request.Form) + x1, x2 := solve(floats) + answer := formatSolutions(x1, x2) + fmt.Fprintf(writer, solution, question, answer) + } else if message != "" { + fmt.Fprintf(writer, error, message) + } + } + fmt.Fprint(writer, pageBottom) +} + +func processRequest(request *http.Request) ([3]float64, string, bool) { + var floats [3]float64 + count := 0 + for index, key := range []string{"a", "b", "c"} { + if slice, found := request.Form[key]; found && len(slice) > 0 { + if slice[0] != "" { + if x, err := strconv.ParseFloat(slice[0], 64); + err != nil { + return floats, "'" + slice[0] + "' is invalid", false + } else { + floats[index] = x + } + } else { // as a courtesy to users treat blanks as 0 + request.Form[key][0] = "0" + floats[index] = 0 + } + count++ + } + } + if count != 3 { // the first time the form is empty; + return floats, "", false // this isn't an error but there's + } // nothing to calculate + if EqualFloat(floats[0], 0, -1) { + return floats, "the x² factor may not be 0", false + } + return floats, "", true +} + +func formatQuestion(form map[string][]string) string { + result := formatSignAndNumber("", form["a"][0], "x²") + result += formatSignAndNumber(" ", form["b"][0], "x") + result += formatSignAndNumber(" ", form["c"][0], "") + return result +} + +func formatSignAndNumber(signPad, number, suffix string) string { + if number == "" || number == "0" || number == "0.0" { + return "" + } + var sign string + if signPad != "" { + sign = signPad + "+" + signPad + } + if number[0] == '-' { + sign = signPad + "-" + signPad + number = number[1:] + } + if suffix != "" && number == "1" { + return sign + suffix + } + return sign + number + suffix +} + +func formatSolutions(x1, x2 complex128) string { + exactlyOneSolution := false + if cmplx.IsNaN(x1) && cmplx.IsNaN(x2) { + return noSolution + } + if cmplx.IsNaN(x1) { + exactlyOneSolution = true + x1 = x2 + } else if cmplx.IsNaN(x2) || EqualComplex(x1, x2) { + exactlyOneSolution = true + } + if exactlyOneSolution { + return fmt.Sprintf(oneSolution, formatComplex(x1)) + } + return fmt.Sprintf(twoSolutions, formatComplex(x1), formatComplex(x2)) +} + +func formatComplex(x complex128) string { + if EqualFloat(imag(x), 0, -1) { + return fmt.Sprintf("%.*f", decimals, real(x)) + } + return fmt.Sprintf("%.*f", decimals, x) +} + +func solve(floats [3]float64) (complex128, complex128) { + a, b, c := complex(floats[0], 0), complex(floats[1], 0), + complex(floats[2], 0) + root := cmplx.Sqrt(cmplx.Pow(b, 2) - (4 * a * c)) + x1 := (-b + root) / (2 * a) + x2 := (-b - root) / (2 * a) + return x1, x2 +} + +// EqualFloat() returns true if x and y are approximately equal to the +// given limit. Pass a limit of -1 to get the greatest accuracy the machine +// can manage. +func EqualFloat(x, y, limit float64) bool { + if limit <= 0.0 { + limit = math.SmallestNonzeroFloat64 + } + return math.Abs(x-y) <= + (limit * math.Min(math.Abs(x), math.Abs(y))) +} + +func EqualComplex(x, y complex128) bool { + return EqualFloat(real(x), real(y), -1) && + EqualFloat(imag(x), imag(y), -1) +} diff --git a/src/safemap/safemap.go b/src/safemap/safemap.go new file mode 100644 index 0000000..9bb29cd --- /dev/null +++ b/src/safemap/safemap.go @@ -0,0 +1,115 @@ +// Copyright © 2011-12 Qtrac Ltd. +// +// This program or package and any associated files are licensed under the +// Apache License, Version 2.0 (the "License"); you may not use these files +// except in compliance with the License. You can get a copy of the License +// at: http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package safemap + +type safeMap chan commandData + +type commandData struct { + action commandAction + key string + value interface{} + result chan<- interface{} + data chan<- map[string]interface{} + updater UpdateFunc +} + +type commandAction int + +const ( + remove commandAction = iota + end + find + insert + length + update +) + +type findResult struct { + value interface{} + found bool +} + +type SafeMap interface { + Insert(string, interface{}) + Delete(string) + Find(string) (interface{}, bool) + Len() int + Update(string, UpdateFunc) + Close() map[string]interface{} +} + +type UpdateFunc func(interface{}, bool) interface{} + +func New() SafeMap { + sm := make(safeMap) // type safeMap chan commandData + go sm.run() + return sm +} + +func (sm safeMap) run() { + store := make(map[string]interface{}) + for command := range sm { + switch command.action { + case insert: + store[command.key] = command.value + case remove: + delete(store, command.key) + case find: + value, found := store[command.key] + command.result <- findResult{value, found} + case length: + command.result <- len(store) + case update: + value, found := store[command.key] + store[command.key] = command.updater(value, found) + case end: + close(sm) + command.data <- store + } + } +} + +func (sm safeMap) Insert(key string, value interface{}) { + sm <- commandData{action: insert, key: key, value: value} +} + +func (sm safeMap) Delete(key string) { + sm <- commandData{action: remove, key: key} +} + +func (sm safeMap) Find(key string) (value interface{}, found bool) { + reply := make(chan interface{}) + sm <- commandData{action: find, key: key, result: reply} + result := (<-reply).(findResult) + return result.value, result.found +} + +func (sm safeMap) Len() int { + reply := make(chan interface{}) + sm <- commandData{action: length, result: reply} + return (<-reply).(int) +} + +// If the updater calls a safeMap method we will get deadlock! +func (sm safeMap) Update(key string, updater UpdateFunc) { + sm <- commandData{action: update, key: key, updater: updater} +} + +// Close() may only be called once per safe map; all other methods can be +// called as often as desired from any number of goroutines +func (sm safeMap) Close() map[string]interface{} { + reply := make(chan map[string]interface{}) + sm <- commandData{action: end, data: reply} + return <-reply +} diff --git a/src/safemap/safemap_test.go b/src/safemap/safemap_test.go new file mode 100644 index 0000000..c8dabb5 --- /dev/null +++ b/src/safemap/safemap_test.go @@ -0,0 +1,96 @@ +// Copyright © 2011-12 Qtrac Ltd. +// +// This program or package and any associated files are licensed under the +// Apache License, Version 2.0 (the "License"); you may not use these files +// except in compliance with the License. You can get a copy of the License +// at: http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package safemap_test + +import ( + "fmt" + "safemap" + "sync" + "testing" +) + +func TestSafeMap(t *testing.T) { + store := safemap.New() + fmt.Printf("Initially has %d items\n", store.Len()) + + deleted := []int{0, 2, 3, 5, 7, 20, 399, 25, 30, 1000, 91, 97, 98, 99} + + var waiter sync.WaitGroup + + waiter.Add(1) + go func() { // Concurrent Inserter + for i := 0; i < 100; i++ { + store.Insert(fmt.Sprintf("0x%04X", i), i) + if i > 0 && i%15 == 0 { + fmt.Printf("Inserted %d items\n", store.Len()) + } + } + fmt.Printf("Inserted %d items\n", store.Len()) + waiter.Done() + }() + + waiter.Add(1) + go func() { // Concurrent Deleter + for _, i := range deleted { + key := fmt.Sprintf("0x%04X", i) + before := store.Len() + store.Delete(key) + fmt.Printf("Deleted m[%s] (%d) before=%d after=%d\n", + key, i, before, store.Len()) + } + waiter.Done() + }() + + waiter.Add(1) + go func() { // Concurrent Finder + for _, i := range deleted { + for _, j := range []int{i, i + 1} { + key := fmt.Sprintf("0x%04X", j) + value, found := store.Find(key) + if found { + fmt.Printf("Found m[%s] == %d\n", key, value) + } else { + fmt.Printf("Not found m[%s] (%d)\n", key, j) + } + } + } + waiter.Done() + }() + + waiter.Wait() + + updater := func(value interface{}, found bool) interface{} { + if found { + return value.(int) * 1000 + } + return 1 + } + for _, i := range []int{5, 10, 15, 20, 25, 30, 35} { + key := fmt.Sprintf("0x%04X", i) + if value, found := store.Find(key); found { + fmt.Printf("Original m[%s] == %d\t", key, value) + store.Update(key, updater) + if value, found := store.Find(key); found { + fmt.Printf("Updated m[%s] == %5d\n", key, value) + } + } + } + + fmt.Printf("Finished with %d items\n", store.Len()) + // not needed here but useful if you want to free up the goroutine + data := store.Close() + fmt.Println("Closed") + fmt.Printf("len == %d\n", len(data)) + //for k, v := range data { fmt.Printf("%s = %v\n", k, v) } +} diff --git a/src/safeslice/safeslice.go b/src/safeslice/safeslice.go new file mode 100644 index 0000000..1ae8cc8 --- /dev/null +++ b/src/safeslice/safeslice.go @@ -0,0 +1,114 @@ +// Copyright © 2011-12 Qtrac Ltd. +// +// This program or package and any associated files are licensed under the +// Apache License, Version 2.0 (the "License"); you may not use these files +// except in compliance with the License. You can get a copy of the License +// at: http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package safeslice + +type safeSlice chan commandData + +type commandData struct { + action commandAction + index int + item interface{} + result chan<- interface{} + data chan<- []interface{} + updater UpdateFunc +} + +type commandAction int + +const ( + insert commandAction = iota + remove + at + update + end + length +) + +type UpdateFunc func(interface{}) interface{} + +type SafeSlice interface { + Append(interface{}) // Append the given item to the slice + At(int) interface{} // Return the item at the given index position + Close() []interface{} // Close the channel and return the slice + Delete(int) // Delete the item at the given index position + Len() int // Return the number of items in the slice + Update(int, UpdateFunc) // Update the item at the given index position +} + +func New() SafeSlice { + slice := make(safeSlice) + go slice.run() + return slice +} + +func (slice safeSlice) run() { + list := make([]interface{}, 0) + for command := range slice { + switch command.action { + case insert: + list = append(list, command.item) + case remove: // potentially expensive for long lists + if 0 <= command.index && command.index < len(list) { + list = append(list[:command.index], + list[command.index+1:]...) + } + case at: + if 0 <= command.index && command.index < len(list) { + command.result <- list[command.index] + } else { + command.result <- nil + } + case length: + command.result <- len(list) + case update: + if 0 <= command.index && command.index < len(list) { + list[command.index] = command.updater(list[command.index]) + } + case end: + close(slice) + command.data <- list + } + } +} + +func (slice safeSlice) Append(item interface{}) { + slice <- commandData{action: insert, item: item} +} + +func (slice safeSlice) Delete(index int) { + slice <- commandData{action: remove, index: index} +} + +func (slice safeSlice) At(index int) interface{} { + reply := make(chan interface{}) + slice <- commandData{at, index, nil, reply, nil, nil} + return <-reply +} + +func (slice safeSlice) Len() int { + reply := make(chan interface{}) + slice <- commandData{action: length, result: reply} + return (<-reply).(int) +} + +// If the updater calls a safeSlice method we will get deadlock! +func (slice safeSlice) Update(index int, updater UpdateFunc) { + slice <- commandData{action: update, index: index, updater: updater} +} + +func (slice safeSlice) Close() []interface{} { + reply := make(chan []interface{}) + slice <- commandData{action: end, data: reply} + return <-reply +} diff --git a/src/safeslice/safeslice_test.go b/src/safeslice/safeslice_test.go new file mode 100644 index 0000000..2fd0f2b --- /dev/null +++ b/src/safeslice/safeslice_test.go @@ -0,0 +1,84 @@ +// Copyright © 2011-12 Qtrac Ltd. +// +// This program or package and any associated files are licensed under the +// Apache License, Version 2.0 (the "License"); you may not use these files +// except in compliance with the License. You can get a copy of the License +// at: http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package safeslice_test + +import ( + "fmt" + "safeslice" + "sync" + "testing" +) + +func TestSafeSlice(t *testing.T) { + store := safeslice.New() + fmt.Printf("Initially has %d items\n", store.Len()) + + deleted := []int{0, 2, 3, 5, 7, 20, 399, 25, 30, 1000, 91, 97, 98, 99} + + var waiter sync.WaitGroup + + waiter.Add(1) + go func() { // Concurrent Inserter + for i := 0; i < 100; i++ { + store.Append(fmt.Sprintf("%04X", i)) + if i > 0 && i%15 == 0 { + fmt.Printf("Inserted %d items\n", store.Len()) + } + } + fmt.Printf("Inserted %d items\n", store.Len()) + waiter.Done() + }() + + waiter.Add(1) + go func() { // Concurrent Deleter + for _, i := range deleted { + before := store.Len() + store.Delete(i) + fmt.Printf("Deleted m[%d] before=%d after=%d\n", + i, before, store.Len()) + } + waiter.Done() + }() + + waiter.Add(1) + go func() { // Concurrent Finder + for _, i := range deleted { + for _, j := range []int{i, i + 1} { + item := store.At(j) + if item != nil { + fmt.Printf("Found m[%d] == %s\n", j, item) + } else { + fmt.Printf("Not found m[%d]\n", j) + } + } + } + waiter.Done() + }() + + waiter.Wait() + + fmt.Printf("Finished with %d items\n", store.Len()) + updater := func(value interface{}) interface{} { + return value.(string) + ":updated" + } + for i := 0; i < store.Len() && i < 5; i++ { + fmt.Printf("m[%d] == %s -> ", i, store.At(i)) + store.Update(i, updater) + fmt.Printf("%s\n", store.At(i)) + } + list := store.Close() + fmt.Println("Closed") + fmt.Printf("len == %d\n", len(list)) + fmt.Println() +} diff --git a/src/shaper1/shaper1.go b/src/shaper1/shaper1.go new file mode 100644 index 0000000..c7b6bee --- /dev/null +++ b/src/shaper1/shaper1.go @@ -0,0 +1,125 @@ +// Copyright © 2011-12 Qtrac Ltd. +// +// This program or package and any associated files are licensed under the +// Apache License, Version 2.0 (the "License"); you may not use these files +// except in compliance with the License. You can get a copy of the License +// at: http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "fmt" + "image" + "image/color" + "log" + "os" + "shaper1/shapes" +) + +func main() { + log.SetFlags(0) + const width, height = 400, 200 + img := shapes.FilledImage(width, height, + color.RGBA{0xFF, 0xFF, 0xFF, 0xFF}) + x, y := width/4, height/2 + + red := color.RGBA{0xFF, 0, 0, 0xFF} + blue := color.RGBA{0, 0, 0xFF, 0xFF} + // Purely for testing New() vs. New*() + if len(os.Args) == 1 { + fmt.Println("Using NewCircle() & NewRegularPolygon()") + circle := shapes.NewCircle(blue, 90) + circle.SetFill(red) // Uses the aggregated shape.SetFill method + octagon := shapes.NewRegularPolygon(red, 75, 8) + octagon.SetFill(blue) // Uses the aggregated circle.shape.SetFill + polygon := shapes.NewRegularPolygon(image.Black, 65, 4) + if err := shapes.DrawShapes(img, x, y, circle, octagon, polygon); + err != nil { + fmt.Println(err) + } + sanityCheck("circle", circle) + sanityCheck("octagon", octagon) + sanityCheck("polygon", polygon) + } else { + fmt.Println("Using New()") + // The Shapers returned by New can only call + // Shaper methods (Fill(), SetFill(), and Draw()); + // however, we can use type assertion if we need to access other + // methods. + if _, err := shapes.New("Misshapen", shapes.Option{blue, 5}); + err == nil { + fmt.Println("unexpectedly got a non-nil invalid shape!") + } + circle, _ := shapes.New("circle", shapes.Option{blue, 5}) + circle.SetFill(red) + circle.(shapes.CircularShaper).SetRadius(90) + octagon, _ := shapes.New("octagon", shapes.Option{red, 10}) + octagon.SetFill(blue) + // This type assertion changes the original octagon because the new + // octagon is in effect a reference to a shapes.RegularPolygonalShaper + // object + if octagon, ok := octagon.(shapes.RegularPolygonalShaper); ok { + octagon.SetRadius(75) + } + polygon, _ := shapes.New("square", shapes.Option{Radius: 65}) + if err := shapes.DrawShapes(img, x, y, circle, octagon, polygon); + err != nil { + fmt.Println(err) + } + sanityCheck("circle", circle) + sanityCheck("octagon", octagon) + sanityCheck("polygon", polygon) + } + polygon := shapes.NewRegularPolygon(color.RGBA{0, 0x7F, 0, 0xFF}, 65, 4) + showShapeDetails(polygon) + y = 30 + for i, radius := range []int{60, 55, 50, 45, 40} { + polygon.SetRadius(radius) + polygon.SetSides(i + 5) + x += radius + y += height / 8 + if err := shapes.DrawShapes(img, x, y, polygon); err != nil { + fmt.Println(err) + } + } + + filename := "shapes.png" + if err := shapes.SaveImage(img, filename); err != nil { + log.Println(err) + } else { + fmt.Println("Saved", filename) + } + fmt.Println("OK") + + img = shapes.FilledImage(width, height, image.White) + x, y = width/3, height/4 +} + +func sanityCheck(name string, shape shapes.Shaper) { + fmt.Print("name=", name, " ") + fmt.Print("fill=", shape.Fill(), " ") + if shape, ok := shape.(shapes.CircularShaper); ok { + fmt.Print("radius=", shape.Radius(), " ") + if shape, ok := shape.(shapes.RegularPolygonalShaper); ok { + fmt.Print("sides=", shape.Sides(), " ") + } + } + fmt.Println() +} + +func showShapeDetails(shape shapes.Shaper) { + fmt.Print("fill=", shape.Fill(), " ") // All shapes have a fill color + if shape, ok := shape.(shapes.CircularShaper); ok { // shadow variable + fmt.Print("radius=", shape.Radius(), " ") + if shape, ok := shape.(shapes.RegularPolygonalShaper); ok {//shadow + fmt.Print("sides=", shape.Sides(), " ") + } + } + fmt.Println() +} diff --git a/src/shaper1/shapes/shapes.go b/src/shaper1/shapes/shapes.go new file mode 100644 index 0000000..69855a1 --- /dev/null +++ b/src/shaper1/shapes/shapes.go @@ -0,0 +1,351 @@ +// Copyright © 2011-12 Qtrac Ltd. +// +// This program or package and any associated files are licensed under the +// Apache License, Version 2.0 (the "License"); you may not use these files +// except in compliance with the License. You can get a copy of the License +// at: http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package shapes + +import ( + "fmt" + "image" + "image/color" + "image/draw" + "image/jpeg" + "image/png" + "log" + "math" + "os" + "path/filepath" + "runtime" + "strings" +) + +var saneLength, saneRadius, saneSides func(int) int + +func init() { + saneLength = makeBoundedIntFunc(1, 4096) + saneRadius = makeBoundedIntFunc(1, 1024) + saneSides = makeBoundedIntFunc(3, 60) +} + +func makeBoundedIntFunc(minimum, maximum int) func(int) int { + return func(x int) int { + valid := x + switch { + case x < minimum: + valid = minimum + case x > maximum: + valid = maximum + } + if valid != x { + log.Printf("%s(): replaced %d with %d\n", caller(1), x, valid) + } + return valid + } +} + +type Shaper interface { + Fill() color.Color + SetFill(fill color.Color) + Draw(img draw.Image, x, y int) error +} + +type CircularShaper interface { + Shaper // Fill(); SetFill(); Draw() + Radius() int + SetRadius(radius int) +} + +type RegularPolygonalShaper interface { + CircularShaper // Fill(); SetFill(); Draw(); Radius(); SetRadius() + Sides() int + SetSides(sides int) +} + +/* + This is unexported so that we are forced to use NewCircle() to create + one thus ensuring that we always start with valid values since the zero + values are not acceptable in this case. Of course the privacy can only + be enforced outside the shapes package. + newShape() is unexported since we don't want undrawable shapes to be + created. +*/ +type shape struct{ fill color.Color } + +func newShape(fill color.Color) shape { + if fill == nil { // We silently treat a nil color as black + fill = color.Black + } + return shape{fill} +} + +func (shape shape) Fill() color.Color { return shape.fill } + +func (shape *shape) SetFill(fill color.Color) { + if fill == nil { // We silently treat a nil color as black + fill = color.Black + } + shape.fill = fill +} + +// The zero value is invalid! Use NewCircle() to create a valid Circle. +type Circle struct { + shape + radius int +} + +// By calling newShape() we pass on any checking to newShape() without +// having to know what if any is required. +func NewCircle(fill color.Color, radius int) *Circle { + return &Circle{newShape(fill), saneRadius(radius)} +} + +func (circle *Circle) Radius() int { + return circle.radius +} + +func (circle *Circle) SetRadius(radius int) { + circle.radius = saneRadius(radius) +} + +func (circle *Circle) Draw(img draw.Image, x, y int) error { + // Algorithm taken from + // http://en.wikipedia.org/wiki/Midpoint_circle_algorithm + // No need to check the radius is in bounds because you can only + // create circles using NewCircle() which guarantees it is within + // bounds. But the x, y might be outside the image so we check. + if err := checkBounds(img, x, y); err != nil { + return err + } + fill, radius := circle.fill, circle.radius + x0, y0 := x, y + f := 1 - radius + ddF_x, ddF_y := 1, -2*radius + x, y = 0, radius + + img.Set(x0, y0+radius, fill) + img.Set(x0, y0-radius, fill) + img.Set(x0+radius, y0, fill) + img.Set(x0-radius, y0, fill) + + for x < y { + if f >= 0 { + y-- + ddF_y += 2 + f += ddF_y + } + x++ + ddF_x += 2 + f += ddF_x + img.Set(x0+x, y0+y, fill) + img.Set(x0-x, y0+y, fill) + img.Set(x0+x, y0-y, fill) + img.Set(x0-x, y0-y, fill) + img.Set(x0+y, y0+x, fill) + img.Set(x0-y, y0+x, fill) + img.Set(x0+y, y0-x, fill) + img.Set(x0-y, y0-x, fill) + } + return nil +} + +func (circle *Circle) String() string { + return fmt.Sprintf("circle(fill=%v, radius=%d)", circle.fill, + circle.radius) +} + +func checkBounds(img image.Image, x, y int) error { + if !image.Rect(x, y, x, y).In(img.Bounds()) { + return fmt.Errorf("%s(): point (%d, %d) is outside the image\n", + caller(1), x, y) + } + return nil +} + +func caller(steps int) string { + name := "?" + if pc, _, _, ok := runtime.Caller(steps + 1); ok { + name = filepath.Base(runtime.FuncForPC(pc).Name()) + } + return name +} + +// The zero value is invalid! Use NewRegularPolygon() to create a valid +// RegularPolygon. +type RegularPolygon struct { + *Circle + sides int +} + +func NewRegularPolygon(fill color.Color, radius, + sides int) *RegularPolygon { + // By calling NewCircle() we pass on any checking (e.g., bounds + // checking) to NewCircle() without having to know what if any is + // required. + return &RegularPolygon{NewCircle(fill, radius), saneSides(sides)} +} + +func (polygon *RegularPolygon) Sides() int { + return polygon.sides +} + +func (polygon *RegularPolygon) SetSides(sides int) { + polygon.sides = saneSides(sides) +} + +func (polygon *RegularPolygon) Draw(img draw.Image, x, y int) error { + // No need to check the radius or sides are in bounds because you can + // only create polygons using NewRegularPolygon() which guarantees they are + // within bounds. But the x, y might be outside the image so we check. + // len(points) == sides + 1 + if err := checkBounds(img, x, y); err != nil { + return err + } + points := getPoints(x, y, polygon.sides, float64(polygon.Radius())) + for i := 0; i < polygon.sides; i++ { // Draw lines between the apexes + drawLine(img, points[i], points[i+1], polygon.Fill()) + } + return nil +} + +func getPoints(x, y, sides int, radius float64) []image.Point { + points := make([]image.Point, sides+1) + // Compute the shape's apexes (thanks to Jasmin Blanchette) + fullCircle := 2 * math.Pi + x0, y0 := float64(x), float64(y) + for i := 0; i < sides; i++ { + θ := float64(float64(i) * fullCircle / float64(sides)) + x1 := x0 + (radius * math.Sin(θ)) + y1 := y0 + (radius * math.Cos(θ)) + points[i] = image.Pt(int(x1), int(y1)) + } + points[sides] = points[0] // close the shape + return points +} + +// Based on my Perl Image::Base.pm module's line() method +func drawLine(img draw.Image, start, end image.Point, + fill color.Color) { + x0, x1 := start.X, end.X + y0, y1 := start.Y, end.Y + Δx := math.Abs(float64(x1 - x0)) + Δy := math.Abs(float64(y1 - y0)) + if Δx >= Δy { // shallow slope + if x0 > x1 { + x0, y0, x1, y1 = x1, y1, x0, y0 + } + y := y0 + yStep := 1 + if y0 > y1 { + yStep = -1 + } + remainder := float64(int(Δx/2)) - Δx + for x := x0; x <= x1; x++ { + img.Set(x, y, fill) + remainder += Δy + if remainder >= 0.0 { + remainder -= Δx + y += yStep + } + } + } else { // steep slope + if y0 > y1 { + x0, y0, x1, y1 = x1, y1, x0, y0 + } + x := x0 + xStep := 1 + if x0 > x1 { + xStep = -1 + } + remainder := float64(int(Δy/2)) - Δy + for y := y0; y <= y1; y++ { + img.Set(x, y, fill) + remainder += Δx + if remainder >= 0.0 { + remainder -= Δy + x += xStep + } + } + } +} + +func (polygon *RegularPolygon) String() string { + return fmt.Sprintf("polygon(fill=%v, radius=%d, sides=%d)", + polygon.Fill(), polygon.Radius(), polygon.sides) +} + +type Option struct { + Fill color.Color + Radius int +} + +// Factory function: The returned Shaper can only call +// Shaper methods unless we use type assertion: see main() in +// ../main.go for examples. +// Note that this function could return a CircularShaper (in which case +// no type assertion would be needed to set the Radius); but we prefer the +// to be more general since that allows us to add other non-CircularShaper +// shapes later without requiring existing callers to be changed. +func New(shape string, option Option) (Shaper, error) { + sidesForShape := map[string]int{"triangle": 3, "square": 4, + "pentagon": 5, "hexagon": 6, "heptagon": 7, "octagon": 8, + "enneagon": 9, "nonagon": 9, "decagon": 10} + if sides, found := sidesForShape[shape]; found { + return NewRegularPolygon(option.Fill, option.Radius, sides), nil + } + if shape != "circle" { + return nil, fmt.Errorf("shapes.New(): invalid shape '%s'", shape) + } + return NewCircle(option.Fill, option.Radius), nil +} + +func FilledImage(width, height int, fill color.Color) draw.Image { + if fill == nil { // We silently treat a nil color as black + fill = color.Black + } + width = saneLength(width) + height = saneLength(height) + img := image.NewRGBA(image.Rect(0, 0, width, height)) + draw.Draw(img, img.Bounds(), &image.Uniform{fill}, image.ZP, draw.Src) + return img +} + +func DrawShapes(img draw.Image, x, y int, shapes ...Shaper) error { + for _, shape := range shapes { + if err := shape.Draw(img, x, y); err != nil { + return err + } + // Thicker so that it shows up better in screenshots + if err := shape.Draw(img, x+1, y); err != nil { + return err + } + if err := shape.Draw(img, x, y+1); err != nil { + return err + } + } + return nil +} + +func SaveImage(img image.Image, filename string) error { + file, err := os.Create(filename) + if err != nil { + return err + } + defer file.Close() + switch strings.ToLower(filepath.Ext(filename)) { + case ".jpg", ".jpeg": + return jpeg.Encode(file, img, nil) + case ".png": + return png.Encode(file, img) + } + return fmt.Errorf("shapes.SaveImage(): '%s' has an unrecognized "+ + "suffix", filename) +} diff --git a/src/shaper2/shaper2.go b/src/shaper2/shaper2.go new file mode 100644 index 0000000..f825d15 --- /dev/null +++ b/src/shaper2/shaper2.go @@ -0,0 +1,121 @@ +// Copyright © 2011-12 Qtrac Ltd. +// +// This program or package and any associated files are licensed under the +// Apache License, Version 2.0 (the "License"); you may not use these files +// except in compliance with the License. You can get a copy of the License +// at: http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "fmt" + "image" + "image/color" + "log" + "os" + "shaper2/shapes" +) + +func main() { + log.SetFlags(0) + const width, height = 400, 200 + img := shapes.FilledImage(width, height, + color.RGBA{0xFF, 0xFF, 0xFF, 0xFF}) + x, y := width/4, height/2 + + red := color.RGBA{0xFF, 0, 0, 0xFF} + blue := color.RGBA{0, 0, 0xFF, 0xFF} + // Purely for testing New() vs. New*() + if len(os.Args) == 1 { + fmt.Println("Using NewCircle() & NewRegularPolygon()") + circle := shapes.NewCircle(blue, 90) + circle.SetFill(red) // Uses the aggregated shape.SetFill method + octagon := shapes.NewRegularPolygon(red, 75, 8) + octagon.SetFill(blue) // Uses the aggregated circle.shape.SetFill + polygon := shapes.NewRegularPolygon(image.Black, 65, 4) + if err := shapes.DrawShapes(img, x, y, circle, octagon, polygon); + err != nil { + fmt.Println(err) + } + sanityCheck("circle", circle) + sanityCheck("octagon", octagon) + sanityCheck("polygon", polygon) + } else { + fmt.Println("Using New()") + // The Shapers returned by New can only call Shaper methods + // (Draw(), Fill(), SetFill()); however, we can use type + // assertion if we need to access other methods. + if _, err := shapes.New("Misshapen", shapes.Option{blue, 5}); + err == nil { + fmt.Println("unexpectedly got a non-nil invalid shape!") + } + circle, _ := shapes.New("circle", shapes.Option{blue, 5}) + circle.SetFill(red) + circle.(shapes.Radiuser).SetRadius(90) + octagon, _ := shapes.New("octagon", shapes.Option{red, 10}) + octagon.SetFill(blue) + if octagon, ok := octagon.(shapes.Radiuser); ok { + octagon.SetRadius(75) + } + polygon, _ := shapes.New("square", shapes.Option{Radius: 65}) + if err := shapes.DrawShapes(img, x, y, circle, octagon, polygon); + err != nil { + fmt.Println(err) + } + sanityCheck("circle", circle) + sanityCheck("octagon", octagon) + sanityCheck("polygon", polygon) + } + polygon := shapes.NewRegularPolygon(color.RGBA{0, 0x7F, 0, 0xFF}, 65, 4) + showShapeDetails(polygon) + y = 30 + for i, radius := range []int{60, 55, 50, 45, 40} { + polygon.SetRadius(radius) + polygon.SetSides(i + 5) + x += radius + y += height / 8 + if err := shapes.DrawShapes(img, x, y, polygon); err != nil { + fmt.Println(err) + } + } + + filename := "shapes.png" + if err := shapes.SaveImage(img, filename); err != nil { + log.Println(err) + } else { + fmt.Println("Saved", filename) + } + fmt.Println("OK") + + img = shapes.FilledImage(width, height, image.White) + x, y = width/3, height/4 +} + +func sanityCheck(name string, shape shapes.Shaper) { + fmt.Print("name=", name, " ") + fmt.Print("fill=", shape.Fill(), " ") + if shape, ok := shape.(shapes.Radiuser); ok { + fmt.Print("radius=", shape.Radius(), " ") + } + if shape, ok := shape.(shapes.Sideser); ok { + fmt.Print("sides=", shape.Sides(), " ") + } + fmt.Println() +} + +func showShapeDetails(shape shapes.Shaper) { + fmt.Print("fill=", shape.Fill(), " ") // All shapes have a fill color + if shape, ok := shape.(shapes.Radiuser); ok { // shadow variable + fmt.Print("radius=", shape.Radius(), " ") + } + if shape, ok := shape.(shapes.Sideser); ok { // shadow variable + fmt.Print("sides=", shape.Sides(), " ") + } + fmt.Println() +} diff --git a/src/shaper2/shapes/shapes.go b/src/shaper2/shapes/shapes.go new file mode 100644 index 0000000..b62c2ea --- /dev/null +++ b/src/shaper2/shapes/shapes.go @@ -0,0 +1,358 @@ +// Copyright © 2011-12 Qtrac Ltd. +// +// This program or package and any associated files are licensed under the +// Apache License, Version 2.0 (the "License"); you may not use these files +// except in compliance with the License. You can get a copy of the License +// at: http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package shapes + +import ( + "fmt" + "image" + "image/color" + "image/draw" + "image/jpeg" + "image/png" + "log" + "math" + "os" + "path/filepath" + "runtime" + "strings" +) + +var saneLength, saneRadius, saneSides func(int) int + +func init() { + saneLength = makeBoundedIntFunc(1, 4096) + saneRadius = makeBoundedIntFunc(1, 1024) + saneSides = makeBoundedIntFunc(3, 60) +} + +func makeBoundedIntFunc(minimum, maximum int) func(int) int { + return func(x int) int { + valid := x + switch { + case x < minimum: + valid = minimum + case x > maximum: + valid = maximum + } + if valid != x { + log.Printf("%s(): replaced %d with %d\n", caller(1), x, valid) + } + return valid + } +} + +type Shaper interface { + Drawer // Draw() + Filler // Fill(); SetFill() +} + +type Drawer interface { + Draw(img draw.Image, x, y int) error +} + +type Filler interface { + Fill() color.Color + SetFill(fill color.Color) +} + +type Radiuser interface { + Radius() int + SetRadius(radius int) +} + +type Sideser interface { + Sides() int + SetSides(sides int) +} + +/* + This is unexported so that we are forced to use NewCircle() to create + one thus ensuring that we always start with valid values since the zero + values are not acceptable in this case. Of course the privacy can only + be enforced outside the shapes package. + newShape() is unexported since we don't want undrawable shapes to be + created. + This satisfies the Filler interface. +*/ +type shape struct{ fill color.Color } + +func newShape(fill color.Color) shape { + if fill == nil { // We silently treat a nil color as black + fill = color.Black + } + return shape{fill} +} + +func (shape shape) Fill() color.Color { return shape.fill } + +func (shape *shape) SetFill(fill color.Color) { + if fill == nil { // We silently treat a nil color as black + fill = color.Black + } + shape.fill = fill +} + +// The zero value is invalid! Use NewCircle() to create a valid Circle. +type Circle struct { + shape + radius int +} + +// By calling newShape() we pass on any checking to newShape() without +// having to know what if any is required. This satisfies the Filler, +// Radiuser, Drawer, and Stringer interfaces. +func NewCircle(fill color.Color, radius int) *Circle { + return &Circle{newShape(fill), saneRadius(radius)} +} + +func (circle *Circle) Radius() int { + return circle.radius +} + +func (circle *Circle) SetRadius(radius int) { + circle.radius = saneRadius(radius) +} + +func (circle *Circle) Draw(img draw.Image, x, y int) error { + // Algorithm taken from + // http://en.wikipedia.org/wiki/Midpoint_circle_algorithm + // No need to check the radius is in bounds because you can only + // create circles using NewCircle() which guarantees it is within + // bounds. But the x, y might be outside the image so we check. + if err := checkBounds(img, x, y); err != nil { + return err + } + fill, radius := circle.fill, circle.radius + x0, y0 := x, y + f := 1 - radius + ddF_x, ddF_y := 1, -2*radius + x, y = 0, radius + + img.Set(x0, y0+radius, fill) + img.Set(x0, y0-radius, fill) + img.Set(x0+radius, y0, fill) + img.Set(x0-radius, y0, fill) + + for x < y { + if f >= 0 { + y-- + ddF_y += 2 + f += ddF_y + } + x++ + ddF_x += 2 + f += ddF_x + img.Set(x0+x, y0+y, fill) + img.Set(x0-x, y0+y, fill) + img.Set(x0+x, y0-y, fill) + img.Set(x0-x, y0-y, fill) + img.Set(x0+y, y0+x, fill) + img.Set(x0-y, y0+x, fill) + img.Set(x0+y, y0-x, fill) + img.Set(x0-y, y0-x, fill) + } + return nil +} + +func (circle *Circle) String() string { + return fmt.Sprintf("circle(fill=%v, radius=%d)", circle.fill, + circle.radius) +} + +func checkBounds(img image.Image, x, y int) error { + if !image.Rect(x, y, x, y).In(img.Bounds()) { + return fmt.Errorf("%s(): point (%d, %d) is outside the image\n", + caller(1), x, y) + } + return nil +} + +func caller(steps int) string { + name := "?" + if pc, _, _, ok := runtime.Caller(steps + 1); ok { + name = filepath.Base(runtime.FuncForPC(pc).Name()) + } + return name +} + +// The zero value is invalid! Use NewRegularPolygon() to create a valid +// RegularPolygon. +type RegularPolygon struct { + *Circle + sides int +} + + +// This satisfies the Filler, Radiuser, Drawer, Sideser, and Stringer +// interfaces. +func NewRegularPolygon(fill color.Color, radius, + sides int) *RegularPolygon { + // By calling NewCircle() we pass on any checking (e.g., bounds + // checking) to NewCircle() without having to know what if any is + // required. + return &RegularPolygon{NewCircle(fill, radius), saneSides(sides)} +} + +func (polygon *RegularPolygon) Sides() int { + return polygon.sides +} + +func (polygon *RegularPolygon) SetSides(sides int) { + polygon.sides = saneSides(sides) +} + +func (polygon *RegularPolygon) Draw(img draw.Image, x, y int) error { + // No need to check the radius or sides are in bounds because you can + // only create polygons using NewRegularPolygon() which guarantees they + // are within bounds. But the x, y might be outside the image so we + // check. len(points) == sides + 1 + if err := checkBounds(img, x, y); err != nil { + return err + } + points := getPoints(x, y, polygon.sides, float64(polygon.Radius())) + for i := 0; i < polygon.sides; i++ { // Draw lines between the apexes + drawLine(img, points[i], points[i+1], polygon.Fill()) + } + return nil +} + +func getPoints(x, y, sides int, radius float64) []image.Point { + points := make([]image.Point, sides+1) + // Compute the shape's apexes (thanks to Jasmin Blanchette) + fullCircle := 2 * math.Pi + x0, y0 := float64(x), float64(y) + for i := 0; i < sides; i++ { + θ := float64(float64(i) * fullCircle / float64(sides)) + x1 := x0 + (radius * math.Sin(θ)) + y1 := y0 + (radius * math.Cos(θ)) + points[i] = image.Pt(int(x1), int(y1)) + } + points[sides] = points[0] // close the shape + return points +} + +// Based on my Perl Image::Base.pm module's line() method +func drawLine(img draw.Image, start, end image.Point, + fill color.Color) { + x0, x1 := start.X, end.X + y0, y1 := start.Y, end.Y + Δx := math.Abs(float64(x1 - x0)) + Δy := math.Abs(float64(y1 - y0)) + if Δx >= Δy { // shallow slope + if x0 > x1 { + x0, y0, x1, y1 = x1, y1, x0, y0 + } + y := y0 + yStep := 1 + if y0 > y1 { + yStep = -1 + } + remainder := float64(int(Δx/2)) - Δx + for x := x0; x <= x1; x++ { + img.Set(x, y, fill) + remainder += Δy + if remainder >= 0.0 { + remainder -= Δx + y += yStep + } + } + } else { // steep slope + if y0 > y1 { + x0, y0, x1, y1 = x1, y1, x0, y0 + } + x := x0 + xStep := 1 + if x0 > x1 { + xStep = -1 + } + remainder := float64(int(Δy/2)) - Δy + for y := y0; y <= y1; y++ { + img.Set(x, y, fill) + remainder += Δx + if remainder >= 0.0 { + remainder -= Δy + x += xStep + } + } + } +} + +func (polygon *RegularPolygon) String() string { + return fmt.Sprintf("polygon(fill=%v, radius=%d, sides=%d)", + polygon.Fill(), polygon.Radius(), polygon.sides) +} + +type Option struct { + Fill color.Color + Radius int +} + +// Factory function: The returned Shaper can only call +// Shaper methods unless we use type assertion: see main() in +// ../main.go for examples. +func New(shape string, option Option) (Shaper, error) { + sidesForShape := map[string]int{"triangle": 3, "square": 4, + "pentagon": 5, "hexagon": 6, "heptagon": 7, "octagon": 8, + "enneagon": 9, "nonagon": 9, "decagon": 10} + if sides, found := sidesForShape[shape]; found { + return NewRegularPolygon(option.Fill, option.Radius, sides), nil + } + if shape != "circle" { + return nil, fmt.Errorf("shapes.New(): invalid shape '%s'", shape) + } + return NewCircle(option.Fill, option.Radius), nil +} + +func FilledImage(width, height int, fill color.Color) draw.Image { + if fill == nil { // We silently treat a nil color as black + fill = color.Black + } + width = saneLength(width) + height = saneLength(height) + img := image.NewRGBA(image.Rect(0, 0, width, height)) + draw.Draw(img, img.Bounds(), &image.Uniform{fill}, image.ZP, draw.Src) + return img +} + +func DrawShapes(img draw.Image, x, y int, shapes ...Drawer) error { + for _, shape := range shapes { + if err := shape.Draw(img, x, y); err != nil { + return err + } + // Thicker so that it shows up better in screenshots + if err := shape.Draw(img, x+1, y); err != nil { + return err + } + if err := shape.Draw(img, x, y+1); err != nil { + return err + } + } + return nil +} + +func SaveImage(img image.Image, filename string) error { + file, err := os.Create(filename) + if err != nil { + return err + } + defer file.Close() + switch strings.ToLower(filepath.Ext(filename)) { + case ".jpg", ".jpeg": + return jpeg.Encode(file, img, nil) + case ".png": + return png.Encode(file, img) + } + return fmt.Errorf("shapes.SaveImage(): '%s' has an unrecognized "+ + "suffix", filename) +} diff --git a/src/shaper3/shaper3.go b/src/shaper3/shaper3.go new file mode 100644 index 0000000..9f1bdaa --- /dev/null +++ b/src/shaper3/shaper3.go @@ -0,0 +1,120 @@ +// Copyright © 2011-12 Qtrac Ltd. +// +// This program or package and any associated files are licensed under the +// Apache License, Version 2.0 (the "License"); you may not use these files +// except in compliance with the License. You can get a copy of the License +// at: http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "fmt" + "image" + "image/color" + "log" + "os" + "shaper3/shapes" +) + +func main() { + log.SetFlags(0) + const width, height = 400, 200 + img := shapes.FilledImage(width, height, + color.RGBA{0xFF, 0xFF, 0xFF, 0xFF}) + x, y := width/4, height/2 + + red := color.RGBA{0xFF, 0, 0, 0xFF} + blue := color.RGBA{0, 0, 0xFF, 0xFF} + // Purely for testing New() vs. New*() + if len(os.Args) == 1 { + fmt.Println("Using NewCircle() & NewRegularPolygon()") + circle := shapes.Circle{blue, 90} + circle.Color = red + octagon := shapes.RegularPolygon{red, 75, 8} + octagon.Color = blue + polygon := shapes.RegularPolygon{image.Black, 65, 4} + if err := shapes.DrawShapes(img, x, y, circle, octagon, polygon); + err != nil { + fmt.Println(err) + } + sanityCheck("circle", circle) + sanityCheck("octagon", octagon) + sanityCheck("polygon", polygon) + } else { + fmt.Println("Using New()") + if _, err := shapes.New("Misshapen", shapes.Option{blue, 5}); + err == nil { + fmt.Println("unexpectedly got a non-nil invalid shape!") + } + shape, _ := shapes.New("circle", shapes.Option{blue, 5}) + circle := shape.(shapes.Circle) + circle.Color = red + circle.Radius = 90 + shape, _ = shapes.New("octagon", shapes.Option{red, 10}) + octagon := shape.(shapes.RegularPolygon) + octagon.Color = blue + octagon.Radius = 75 + polygon, _ := shapes.New("square", shapes.Option{Radius: 65}) + if err := shapes.DrawShapes(img, x, y, circle, octagon, polygon); + err != nil { + fmt.Println(err) + } + sanityCheck("circle", circle) + sanityCheck("octagon", octagon) + sanityCheck("polygon", polygon) + } + polygon := shapes.RegularPolygon{color.RGBA{0, 0x7F, 0, 0xFF}, 65, 4} + sanityCheck("polygon", polygon) + y = 30 + for i, radius := range []int{60, 55, 50, 45, 40} { + polygon.Radius = radius + polygon.Sides = i + 5 + x += radius + y += height / 8 + if err := shapes.DrawShapes(img, x, y, polygon); err != nil { + fmt.Println(err) + } + } + + filename := "shapes.png" + if err := shapes.SaveImage(img, filename); err != nil { + log.Println(err) + } else { + fmt.Println("Saved", filename) + } + fmt.Println("OK") + + img = shapes.FilledImage(width, height, image.White) + x, y = width/3, height/4 +} + +func sanityCheck(name string, drawer shapes.Drawer) { + fmt.Print("name=", name, " ") + var fill color.Color + radius, sides := -1, -1 + if circle, ok := drawer.(shapes.Circle); ok { + fill = circle.Color + radius = circle.Radius + } + if polygon, ok := drawer.(shapes.RegularPolygon); ok { + fill = polygon.Color + radius = polygon.Radius + sides = polygon.Sides + } + if fill != nil { + fmt.Print("fill=", fill, " ") + } + if radius != -1 { + fmt.Print("radius=", radius, " ") + } + if sides != -1 { + fmt.Print("sides=", sides, " ") + } + fmt.Println() +} diff --git a/src/shaper3/shapes/shapes.go b/src/shaper3/shapes/shapes.go new file mode 100644 index 0000000..c5209f7 --- /dev/null +++ b/src/shaper3/shapes/shapes.go @@ -0,0 +1,269 @@ +// Copyright © 2011-12 Qtrac Ltd. +// +// This program or package and any associated files are licensed under the +// Apache License, Version 2.0 (the "License"); you may not use these files +// except in compliance with the License. You can get a copy of the License +// at: http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package shapes + +import ( + "fmt" + "image" + "image/color" + "image/draw" + "image/jpeg" + "image/png" + "math" + "os" + "path/filepath" + "runtime" + "strings" +) + + +func clamp(minimum, x, maximum int) int { + switch { + case x < minimum: + return minimum + case x > maximum: + return maximum + } + return x +} + +func validFillColor(fill color.Color) color.Color { + if fill == nil { // We silently treat a nil color as black + return color.Black + } + return fill +} + +type Drawer interface { + Draw(img draw.Image, x, y int) error +} + +type Circle struct { + color.Color + Radius int +} + +func (circle Circle) Draw(img draw.Image, x, y int) error { + // Algorithm taken from + // http://en.wikipedia.org/wiki/Midpoint_circle_algorithm + // No need to check the radius is in bounds because you can only + // create circles using NewCircle() which guarantees it is within + // bounds. But the x, y might be outside the image so we check. + if err := checkBounds(img, x, y); err != nil { + return err + } + fill := validFillColor(circle.Color) + radius := clamp(1, circle.Radius, 1024) + + x0, y0 := x, y + f := 1 - radius + ddF_x, ddF_y := 1, -2*radius + x, y = 0, radius + + img.Set(x0, y0+radius, fill) + img.Set(x0, y0-radius, fill) + img.Set(x0+radius, y0, fill) + img.Set(x0-radius, y0, fill) + + for x < y { + if f >= 0 { + y-- + ddF_y += 2 + f += ddF_y + } + x++ + ddF_x += 2 + f += ddF_x + img.Set(x0+x, y0+y, fill) + img.Set(x0-x, y0+y, fill) + img.Set(x0+x, y0-y, fill) + img.Set(x0-x, y0-y, fill) + img.Set(x0+y, y0+x, fill) + img.Set(x0-y, y0+x, fill) + img.Set(x0+y, y0-x, fill) + img.Set(x0-y, y0-x, fill) + } + return nil +} + +func (circle Circle) String() string { + return fmt.Sprintf("circle(fill=%v, radius=%d)", circle.Color, + circle.Radius) +} + +func checkBounds(img image.Image, x, y int) error { + if !image.Rect(x, y, x, y).In(img.Bounds()) { + return fmt.Errorf("%s(): point (%d, %d) is outside the image\n", + caller(1), x, y) + } + return nil +} + +func caller(steps int) string { + name := "?" + if pc, _, _, ok := runtime.Caller(steps + 1); ok { + name = filepath.Base(runtime.FuncForPC(pc).Name()) + } + return name +} + +type RegularPolygon struct { + color.Color + Radius int + Sides int +} + + +func (polygon RegularPolygon) Draw(img draw.Image, x, y int) error { + // No need to check the radius or sides are in bounds because you can + // only create polygons using NewRegularPolygon() which guarantees they + // are within bounds. But the x, y might be outside the image so we + // check. len(points) == sides + 1 + if err := checkBounds(img, x, y); err != nil { + return err + } + fill := validFillColor(polygon.Color) + radius := clamp(1, polygon.Radius, 1024) + sides := clamp(3, polygon.Sides, 60) + points := getPoints(x, y, sides, float64(radius)) + for i := 0; i < sides; i++ { // Draw lines between the apexes + drawLine(img, points[i], points[i+1], fill) + } + return nil +} + +func getPoints(x, y, sides int, radius float64) []image.Point { + points := make([]image.Point, sides+1) + // Compute the shape's apexes (thanks to Jasmin Blanchette) + fullCircle := 2 * math.Pi + x0, y0 := float64(x), float64(y) + for i := 0; i < sides; i++ { + θ := float64(float64(i) * fullCircle / float64(sides)) + x1 := x0 + (radius * math.Sin(θ)) + y1 := y0 + (radius * math.Cos(θ)) + points[i] = image.Pt(int(x1), int(y1)) + } + points[sides] = points[0] // close the shape + return points +} + +// Based on my Perl Image::Base.pm module's line() method +func drawLine(img draw.Image, start, end image.Point, + fill color.Color) { + x0, x1 := start.X, end.X + y0, y1 := start.Y, end.Y + Δx := math.Abs(float64(x1 - x0)) + Δy := math.Abs(float64(y1 - y0)) + if Δx >= Δy { // shallow slope + if x0 > x1 { + x0, y0, x1, y1 = x1, y1, x0, y0 + } + y := y0 + yStep := 1 + if y0 > y1 { + yStep = -1 + } + remainder := float64(int(Δx/2)) - Δx + for x := x0; x <= x1; x++ { + img.Set(x, y, fill) + remainder += Δy + if remainder >= 0.0 { + remainder -= Δx + y += yStep + } + } + } else { // steep slope + if y0 > y1 { + x0, y0, x1, y1 = x1, y1, x0, y0 + } + x := x0 + xStep := 1 + if x0 > x1 { + xStep = -1 + } + remainder := float64(int(Δy/2)) - Δy + for y := y0; y <= y1; y++ { + img.Set(x, y, fill) + remainder += Δx + if remainder >= 0.0 { + remainder -= Δy + x += xStep + } + } + } +} + +func (polygon RegularPolygon) String() string { + return fmt.Sprintf("polygon(fill=%v, radius=%d, sides=%d)", + polygon.Color, polygon.Radius, polygon.Sides) +} + +type Option struct { + Fill color.Color + Radius int +} + +func New(shape string, option Option) (Drawer, error) { + sidesForShape := map[string]int{"triangle": 3, "square": 4, + "pentagon": 5, "hexagon": 6, "heptagon": 7, "octagon": 8, + "enneagon": 9, "nonagon": 9, "decagon": 10} + if sides, found := sidesForShape[shape]; found { + return RegularPolygon{option.Fill, option.Radius, sides}, nil + } + if shape != "circle" { + return nil, fmt.Errorf("shapes.New(): invalid shape '%s'", shape) + } + return Circle{option.Fill, option.Radius}, nil +} + +func FilledImage(width, height int, fill color.Color) draw.Image { + fill = validFillColor(fill) + width = clamp(1, width, 4096) + height = clamp(1, height, 4096) + img := image.NewRGBA(image.Rect(0, 0, width, height)) + draw.Draw(img, img.Bounds(), &image.Uniform{fill}, image.ZP, draw.Src) + return img +} + +func DrawShapes(img draw.Image, x, y int, drawers ...Drawer) error { + for _, drawer := range drawers { + if err := drawer.Draw(img, x, y); err != nil { + return err + } + // Thicker so that it shows up better in screenshots + if err := drawer.Draw(img, x+1, y); err != nil { + return err + } + if err := drawer.Draw(img, x, y+1); err != nil { + return err + } + } + return nil +} + +func SaveImage(img image.Image, filename string) error { + file, err := os.Create(filename) + if err != nil { + return err + } + defer file.Close() + switch strings.ToLower(filepath.Ext(filename)) { + case ".jpg", ".jpeg": + return jpeg.Encode(file, img, nil) + case ".png": + return png.Encode(file, img) + } + return fmt.Errorf("shapes.SaveImage(): '%s' has an unrecognized "+ + "suffix", filename) +} diff --git a/src/shaper_ans1/shaper1.go b/src/shaper_ans1/shaper1.go new file mode 100644 index 0000000..6e61c53 --- /dev/null +++ b/src/shaper_ans1/shaper1.go @@ -0,0 +1,39 @@ +// Copyright © 2011-12 Qtrac Ltd. +// +// This program or package and any associated files are licensed under the +// Apache License, Version 2.0 (the "License"); you may not use these files +// except in compliance with the License. You can get a copy of the License +// at: http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "image" + "image/color" + "shaper_ans1/shapes" +) + +func main() { + img := shapes.FilledImage(420, 220, image.White) + fill := color.RGBA{200, 200, 200, 0xFF} // light gray + for i := 0; i < 10; i++ { + width, height := 40+(20*i), 20+(10*i) + rectangle := shapes.NewRectangle(fill, + image.Rect(0, 0, width, height)) + rectangle.SetFilled(true) + x := 10 + (20 * i) + for j := i / 2; j >= 0; j-- { + rectangle.Draw(img, x+j, (x/2)+j) + } + fill.R -= uint8(i * 5) + fill.G = fill.R + fill.B = fill.R + } + shapes.SaveImage(img, "rectangle.png") +} diff --git a/src/shaper_ans1/shapes/shapes.go b/src/shaper_ans1/shapes/shapes.go new file mode 100644 index 0000000..a450b6e --- /dev/null +++ b/src/shaper_ans1/shapes/shapes.go @@ -0,0 +1,419 @@ +// Copyright © 2011-12 Qtrac Ltd. +// +// This program or package and any associated files are licensed under the +// Apache License, Version 2.0 (the "License"); you may not use these files +// except in compliance with the License. You can get a copy of the License +// at: http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package shapes + +import ( + "fmt" + "image" + "image/color" + "image/draw" + "image/jpeg" + "image/png" + "log" + "math" + "os" + "path/filepath" + "runtime" + "strings" +) + +var saneLength, saneRadius, saneSides func(int) int + +func init() { + saneLength = makeBoundedIntFunc(1, 4096) + saneRadius = makeBoundedIntFunc(1, 1024) + saneSides = makeBoundedIntFunc(3, 60) +} + +func makeBoundedIntFunc(minimum, maximum int) func(int) int { + return func(x int) int { + valid := x + switch { + case x < minimum: + valid = minimum + case x > maximum: + valid = maximum + } + if valid != x { + log.Printf("%s(): replaced %d with %d\n", caller(1), x, valid) + } + return valid + } +} + +func saneRectangle(rect image.Rectangle) image.Rectangle { + rect = rect.Canon() + width, height := rect.Dx(), rect.Dy() + if width < 1 || width > 4096 || height < 1 || height > 4096 { + return image.Rect(0, 0, 16, 16) + } + return rect +} + +type Shaper interface { + Fill() color.Color + SetFill(fill color.Color) + Draw(img draw.Image, x, y int) error +} + +type CircularShaper interface { + Shaper // Fill(); SetFill(); Draw() + Radius() int + SetRadius(radius int) +} + +type RegularPolygonalShaper interface { + CircularShaper // Fill(); SetFill(); Draw(); Radius(); SetRadius() + Sides() int + SetSides(sides int) +} + +type RectangularShaper interface { + Shaper // Fill(); SetFill(); Draw() + Rect() image.Rectangle + SetRect(image.Rectangle) + Filled() bool + SetFilled(bool) +} + +/* + This is unexported so that we are forced to use NewCircle() to create + one thus ensuring that we always start with valid values since the zero + values are not acceptable in this case. Of course the privacy can only + be enforced outside the shapes package. + newShape() is unexported since we don't want undrawable shapes to be + created. +*/ +type shape struct{ fill color.Color } + +func newShape(fill color.Color) shape { + if fill == nil { // We silently treat a nil color as black + fill = color.Black + } + return shape{fill} +} + +func (shape shape) Fill() color.Color { return shape.fill } + +func (shape *shape) SetFill(fill color.Color) { + if fill == nil { // We silently treat a nil color as black + fill = color.Black + } + shape.fill = fill +} + +// The zero value is invalid! Use NewCircle() to create a valid Circle. +type Circle struct { + shape + radius int +} + +// By calling newShape() we pass on any checking to newShape() without +// having to know what if any is required. +func NewCircle(fill color.Color, radius int) *Circle { + return &Circle{newShape(fill), saneRadius(radius)} +} + +func (circle *Circle) Radius() int { + return circle.radius +} + +func (circle *Circle) SetRadius(radius int) { + circle.radius = saneRadius(radius) +} + +func (circle *Circle) Draw(img draw.Image, x, y int) error { + // Algorithm taken from + // http://en.wikipedia.org/wiki/Midpoint_circle_algorithm + // No need to check the radius is in bounds because you can only + // create circles using NewCircle() which guarantees it is within + // bounds. But the x, y might be outside the image so we check. + if err := checkBounds(img, x, y); err != nil { + return err + } + fill, radius := circle.fill, circle.radius + x0, y0 := x, y + f := 1 - radius + ddF_x, ddF_y := 1, -2*radius + x, y = 0, radius + + img.Set(x0, y0+radius, fill) + img.Set(x0, y0-radius, fill) + img.Set(x0+radius, y0, fill) + img.Set(x0-radius, y0, fill) + + for x < y { + if f >= 0 { + y-- + ddF_y += 2 + f += ddF_y + } + x++ + ddF_x += 2 + f += ddF_x + img.Set(x0+x, y0+y, fill) + img.Set(x0-x, y0+y, fill) + img.Set(x0+x, y0-y, fill) + img.Set(x0-x, y0-y, fill) + img.Set(x0+y, y0+x, fill) + img.Set(x0-y, y0+x, fill) + img.Set(x0+y, y0-x, fill) + img.Set(x0-y, y0-x, fill) + } + return nil +} + +func (circle *Circle) String() string { + return fmt.Sprintf("circle(fill=%v, radius=%d)", circle.fill, + circle.radius) +} + +func checkBounds(img image.Image, x, y int) error { + if !image.Rect(x, y, x, y).In(img.Bounds()) { + return fmt.Errorf("%s(): point (%d, %d) is outside the image\n", + caller(1), x, y) + } + return nil +} + +func caller(steps int) string { + name := "?" + if pc, _, _, ok := runtime.Caller(steps + 1); ok { + name = filepath.Base(runtime.FuncForPC(pc).Name()) + } + return name +} + +// The zero value is invalid! Use NewRegularPolygon() to create a valid +// RegularPolygon. +type RegularPolygon struct { + *Circle + sides int +} + +func NewRegularPolygon(fill color.Color, radius, sides int) *RegularPolygon { + // By calling NewCircle() we pass on any checking (e.g., bounds + // checking) to NewCircle() without having to know what if any is + // required. + return &RegularPolygon{NewCircle(fill, radius), saneSides(sides)} +} + +func (polygon *RegularPolygon) Sides() int { + return polygon.sides +} + +func (polygon *RegularPolygon) SetSides(sides int) { + polygon.sides = saneSides(sides) +} + +func (polygon *RegularPolygon) Draw(img draw.Image, x, y int) error { + // No need to check the radius or sides are in bounds because you can + // only create polygons using NewRegularPolygon() which guarantees they + // are within bounds. But the x, y might be outside the image so we + // check. len(points) == sides + 1 + if err := checkBounds(img, x, y); err != nil { + return err + } + points := getPoints(x, y, polygon.sides, float64(polygon.Radius())) + for i := 0; i < polygon.sides; i++ { // Draw lines between the apexes + drawLine(img, points[i], points[i+1], polygon.Fill()) + } + return nil +} + +func getPoints(x, y, sides int, radius float64) []image.Point { + points := make([]image.Point, sides+1) + // Compute the shape's apexes (thanks to Jasmin Blanchette) + fullCircle := 2 * math.Pi + x0, y0 := float64(x), float64(y) + for i := 0; i < sides; i++ { + θ := float64(float64(i) * fullCircle / float64(sides)) + x1 := x0 + (radius * math.Sin(θ)) + y1 := y0 + (radius * math.Cos(θ)) + points[i] = image.Pt(int(x1), int(y1)) + } + points[sides] = points[0] // close the shape + return points +} + +// Based on my Perl Image::Base.pm module's line() method +func drawLine(img draw.Image, start, end image.Point, + fill color.Color) { + x0, x1 := start.X, end.X + y0, y1 := start.Y, end.Y + Δx := math.Abs(float64(x1 - x0)) + Δy := math.Abs(float64(y1 - y0)) + if Δx >= Δy { // shallow slope + if x0 > x1 { + x0, y0, x1, y1 = x1, y1, x0, y0 + } + y := y0 + yStep := 1 + if y0 > y1 { + yStep = -1 + } + remainder := float64(int(Δx/2)) - Δx + for x := x0; x <= x1; x++ { + img.Set(x, y, fill) + remainder += Δy + if remainder >= 0.0 { + remainder -= Δx + y += yStep + } + } + } else { // steep slope + if y0 > y1 { + x0, y0, x1, y1 = x1, y1, x0, y0 + } + x := x0 + xStep := 1 + if x0 > x1 { + xStep = -1 + } + remainder := float64(int(Δy/2)) - Δy + for y := y0; y <= y1; y++ { + img.Set(x, y, fill) + remainder += Δx + if remainder >= 0.0 { + remainder -= Δy + x += xStep + } + } + } +} + +func (polygon *RegularPolygon) String() string { + return fmt.Sprintf("polygon(fill=%v, radius=%d, sides=%d)", + polygon.Fill(), polygon.Radius(), polygon.sides) +} + +// The zero value is invalid! Use NewRectangle() to create a valid +// Rectangle. +type Rectangle struct { + shape + image.Rectangle + filled bool +} + +func NewRectangle(fill color.Color, rect image.Rectangle) *Rectangle { + return &Rectangle{newShape(fill), saneRectangle(rect), false} +} + +func (rectangle *Rectangle) Rect() image.Rectangle { + return rectangle.Rectangle +} + +func (rectangle *Rectangle) SetRect(rect image.Rectangle) { + rectangle.Rectangle = saneRectangle(rect) +} + +func (rectangle *Rectangle) Filled() bool { + return rectangle.filled +} + +func (rectangle *Rectangle) SetFilled(filled bool) { + rectangle.filled = filled +} + +// x, y are the top-left (for radius-based shapes they are the middle) +func (rectangle *Rectangle) Draw(img draw.Image, x, y int) error { + if err := checkBounds(img, x, y); err != nil { + return err + } + fill := rectangle.fill + x0, x1 := rectangle.Rectangle.Min.X, rectangle.Rectangle.Max.X + y0, y1 := rectangle.Rectangle.Min.Y, rectangle.Rectangle.Max.Y + x0 += x + x1 += x + y0 += y + y1 += y + if !rectangle.filled { + drawLine(img, image.Point{x0, y0}, image.Point{x1, y0}, fill) + drawLine(img, image.Point{x0, y1}, image.Point{x1, y1}, fill) + drawLine(img, image.Point{x0, y0}, image.Point{x0, y1}, fill) + drawLine(img, image.Point{x1, y0}, image.Point{x1, y1}, fill) + } else { + draw.Draw(img, image.Rect(x0, y0, x1+1, y1+1), + &image.Uniform{fill}, image.Pt(x0, y0), draw.Src) + } + return nil +} + +type Option struct { + Fill color.Color + Radius int + Rect image.Rectangle + Filled bool +} + +func New(shape string, option Option) (Shaper, error) { + sidesForShape := map[string]int{"triangle": 3, "square": 4, + "pentagon": 5, "hexagon": 6, "heptagon": 7, "octagon": 8, + "enneagon": 9, "nonagon": 9, "decagon": 10} + if sides, found := sidesForShape[shape]; found { + return NewRegularPolygon(option.Fill, option.Radius, sides), nil + } + if shape == "rectangle" { + rect := NewRectangle(option.Fill, option.Rect) + rect.SetFilled(option.Filled) + return rect, nil + } + if shape != "circle" { + return nil, fmt.Errorf("shapes.New(): invalid shape '%s'", shape) + } + return NewCircle(option.Fill, option.Radius), nil +} + +func FilledImage(width, height int, fill color.Color) draw.Image { + if fill == nil { + fill = color.Black + } + width = saneLength(width) + height = saneLength(height) + img := image.NewRGBA(image.Rect(0, 0, width, height)) + draw.Draw(img, img.Bounds(), &image.Uniform{fill}, image.ZP, draw.Src) + return img +} + +func DrawShapes(img draw.Image, x, y int, shapes ...Shaper) error { + for _, shape := range shapes { + if err := shape.Draw(img, x, y); err != nil { + return err + } + // Thicker so that it shows up better in screenshots + if err := shape.Draw(img, x+1, y); err != nil { + return err + } + if err := shape.Draw(img, x, y+1); err != nil { + return err + } + } + return nil +} + +func SaveImage(img image.Image, filename string) error { + file, err := os.Create(filename) + if err != nil { + return err + } + defer file.Close() + switch strings.ToLower(filepath.Ext(filename)) { + case ".jpg", ".jpeg": + return jpeg.Encode(file, img, nil) + case ".png": + return png.Encode(file, img) + } + return fmt.Errorf("shapes.SaveImage(): '%s' has an unrecognized "+ + "suffix", filename) +} diff --git a/src/shaper_ans2/shaper2.go b/src/shaper_ans2/shaper2.go new file mode 100644 index 0000000..5c9673f --- /dev/null +++ b/src/shaper_ans2/shaper2.go @@ -0,0 +1,39 @@ +// Copyright © 2011-12 Qtrac Ltd. +// +// This program or package and any associated files are licensed under the +// Apache License, Version 2.0 (the "License"); you may not use these files +// except in compliance with the License. You can get a copy of the License +// at: http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "image" + "image/color" + "shaper_ans2/shapes" +) + +func main() { + img := shapes.FilledImage(420, 220, image.White) + fill := color.RGBA{200, 200, 200, 0xFF} // light gray + for i := 0; i < 10; i++ { + width, height := 40+(20*i), 20+(10*i) + rectangle := shapes.NewRectangle(fill, + image.Rect(0, 0, width, height)) + rectangle.SetFilled(true) + x := 10 + (20 * i) + for j := i / 2; j >= 0; j-- { + rectangle.Draw(img, x+j, (x/2)+j) + } + fill.R -= uint8(i * 5) + fill.G = fill.R + fill.B = fill.R + } + shapes.SaveImage(img, "rectangle.png") +} diff --git a/src/shaper_ans2/shapes/shapes.go b/src/shaper_ans2/shapes/shapes.go new file mode 100644 index 0000000..131c139 --- /dev/null +++ b/src/shaper_ans2/shapes/shapes.go @@ -0,0 +1,428 @@ +// Copyright © 2011-12 Qtrac Ltd. +// +// This program or package and any associated files are licensed under the +// Apache License, Version 2.0 (the "License"); you may not use these files +// except in compliance with the License. You can get a copy of the License +// at: http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package shapes + +import ( + "fmt" + "image" + "image/color" + "image/draw" + "image/jpeg" + "image/png" + "log" + "math" + "os" + "path/filepath" + "runtime" + "strings" +) + +var saneLength, saneRadius, saneSides func(int) int + +func init() { + saneLength = makeBoundedIntFunc(1, 4096) + saneRadius = makeBoundedIntFunc(1, 1024) + saneSides = makeBoundedIntFunc(3, 60) +} + +func makeBoundedIntFunc(minimum, maximum int) func(int) int { + return func(x int) int { + valid := x + switch { + case x < minimum: + valid = minimum + case x > maximum: + valid = maximum + } + if valid != x { + log.Printf("%s(): replaced %d with %d\n", caller(1), x, valid) + } + return valid + } +} + +func saneRectangle(rect image.Rectangle) image.Rectangle { + rect = rect.Canon() + width, height := rect.Dx(), rect.Dy() + if width < 1 || width > 4096 || height < 1 || height > 4096 { + return image.Rect(0, 0, 16, 16) + } + return rect +} + +type Shaper interface { + Drawer + Filler +} + +type Drawer interface { + Draw(img draw.Image, x, y int) error +} + +type Filler interface { + Fill() color.Color + SetFill(fill color.Color) +} + +type Radiuser interface { + Radius() int + SetRadius(radius int) +} + +type Sideser interface { + Sides() int + SetSides(sides int) +} + +type Rectangler interface { + Rect() image.Rectangle + SetRect(image.Rectangle) +} + +type Filleder interface { + Filled() bool + SetFilled(bool) +} + +/* + This is unexported so that we are forced to use NewCircle() to create + one thus ensuring that we always start with valid values since the zero + values are not acceptable in this case. Of course the privacy can only + be enforced outside the shapes package. + newShape() is unexported since we don't want undrawable shapes to be + created. +*/ +type shape struct{ fill color.Color } + +func newShape(fill color.Color) shape { + if fill == nil { // We silently treat a nil color as black + fill = color.Black + } + return shape{fill} +} + +func (shape shape) Fill() color.Color { return shape.fill } + +func (shape *shape) SetFill(fill color.Color) { + if fill == nil { // We silently treat a nil color as black + fill = color.Black + } + shape.fill = fill +} + +// The zero value is invalid! Use NewCircle() to create a valid Circle. +type Circle struct { + shape + radius int +} + +// By calling newShape() we pass on any checking to newShape() without +// having to know what if any is required. +func NewCircle(fill color.Color, radius int) *Circle { + return &Circle{newShape(fill), saneRadius(radius)} +} + +func (circle *Circle) Radius() int { + return circle.radius +} + +func (circle *Circle) SetRadius(radius int) { + circle.radius = saneRadius(radius) +} + +func (circle *Circle) Draw(img draw.Image, x, y int) error { + // Algorithm taken from + // http://en.wikipedia.org/wiki/Midpoint_circle_algorithm + // No need to check the radius is in bounds because you can only + // create circles using NewCircle() which guarantees it is within + // bounds. But the x, y might be outside the image so we check. + if err := checkBounds(img, x, y); err != nil { + return err + } + fill, radius := circle.fill, circle.radius + x0, y0 := x, y + f := 1 - radius + ddF_x, ddF_y := 1, -2*radius + x, y = 0, radius + + img.Set(x0, y0+radius, fill) + img.Set(x0, y0-radius, fill) + img.Set(x0+radius, y0, fill) + img.Set(x0-radius, y0, fill) + + for x < y { + if f >= 0 { + y-- + ddF_y += 2 + f += ddF_y + } + x++ + ddF_x += 2 + f += ddF_x + img.Set(x0+x, y0+y, fill) + img.Set(x0-x, y0+y, fill) + img.Set(x0+x, y0-y, fill) + img.Set(x0-x, y0-y, fill) + img.Set(x0+y, y0+x, fill) + img.Set(x0-y, y0+x, fill) + img.Set(x0+y, y0-x, fill) + img.Set(x0-y, y0-x, fill) + } + return nil +} + +func (circle *Circle) String() string { + return fmt.Sprintf("circle(fill=%v, radius=%d)", circle.fill, + circle.radius) +} + +func checkBounds(img image.Image, x, y int) error { + if !image.Rect(x, y, x, y).In(img.Bounds()) { + return fmt.Errorf("%s(): point (%d, %d) is outside the image\n", + caller(1), x, y) + } + return nil +} + +func caller(steps int) string { + name := "?" + if pc, _, _, ok := runtime.Caller(steps + 1); ok { + name = filepath.Base(runtime.FuncForPC(pc).Name()) + } + return name +} + +// The zero value is invalid! Use NewRegularPolygon() to create a valid +// RegularPolygon. +type RegularPolygon struct { + *Circle + sides int +} + +func NewRegularPolygon(fill color.Color, radius, + sides int) *RegularPolygon { + // By calling NewCircle() we pass on any checking (e.g., bounds + // checking) to NewCircle() without having to know what if any is + // required. + return &RegularPolygon{NewCircle(fill, radius), saneSides(sides)} +} + +func (polygon *RegularPolygon) Sides() int { + return polygon.sides +} + +func (polygon *RegularPolygon) SetSides(sides int) { + polygon.sides = saneSides(sides) +} + +func (polygon *RegularPolygon) Draw(img draw.Image, x, y int) error { + // No need to check the radius or sides are in bounds because you can + // only create polygons using NewRegularPolygon() which guarantees they + // are within bounds. But the x, y might be outside the image so we + // check. len(points) == sides + 1 + if err := checkBounds(img, x, y); err != nil { + return err + } + points := getPoints(x, y, polygon.sides, float64(polygon.Radius())) + for i := 0; i < polygon.sides; i++ { // Draw lines between the apexes + drawLine(img, points[i], points[i+1], polygon.Fill()) + } + return nil +} + +func getPoints(x, y, sides int, radius float64) []image.Point { + points := make([]image.Point, sides+1) + // Compute the shape's apexes (thanks to Jasmin Blanchette) + fullCircle := 2 * math.Pi + x0, y0 := float64(x), float64(y) + for i := 0; i < sides; i++ { + θ := float64(float64(i) * fullCircle / float64(sides)) + x1 := x0 + (radius * math.Sin(θ)) + y1 := y0 + (radius * math.Cos(θ)) + points[i] = image.Pt(int(x1), int(y1)) + } + points[sides] = points[0] // close the shape + return points +} + +// Based on my Perl Image::Base.pm module's line() method +func drawLine(img draw.Image, start, end image.Point, + fill color.Color) { + x0, x1 := start.X, end.X + y0, y1 := start.Y, end.Y + Δx := math.Abs(float64(x1 - x0)) + Δy := math.Abs(float64(y1 - y0)) + if Δx >= Δy { // shallow slope + if x0 > x1 { + x0, y0, x1, y1 = x1, y1, x0, y0 + } + y := y0 + yStep := 1 + if y0 > y1 { + yStep = -1 + } + remainder := float64(int(Δx/2)) - Δx + for x := x0; x <= x1; x++ { + img.Set(x, y, fill) + remainder += Δy + if remainder >= 0.0 { + remainder -= Δx + y += yStep + } + } + } else { // steep slope + if y0 > y1 { + x0, y0, x1, y1 = x1, y1, x0, y0 + } + x := x0 + xStep := 1 + if x0 > x1 { + xStep = -1 + } + remainder := float64(int(Δy/2)) - Δy + for y := y0; y <= y1; y++ { + img.Set(x, y, fill) + remainder += Δx + if remainder >= 0.0 { + remainder -= Δy + x += xStep + } + } + } +} + +func (polygon *RegularPolygon) String() string { + return fmt.Sprintf("polygon(fill=%v, radius=%d, sides=%d)", + polygon.Fill(), polygon.Radius(), polygon.sides) +} + +// The zero value is invalid! Use NewRectangle() to create a valid +// Rectangle. +type Rectangle struct { + shape + image.Rectangle + filled bool +} + +func NewRectangle(fill color.Color, rect image.Rectangle) *Rectangle { + return &Rectangle{newShape(fill), saneRectangle(rect), false} +} + +func (rectangle *Rectangle) Rect() image.Rectangle { + return rectangle.Rectangle +} + +func (rectangle *Rectangle) SetRect(rect image.Rectangle) { + rectangle.Rectangle = saneRectangle(rect) +} + +func (rectangle *Rectangle) Filled() bool { + return rectangle.filled +} + +func (rectangle *Rectangle) SetFilled(filled bool) { + rectangle.filled = filled +} + +// x, y are the top-left (for radius-based shapes they are the middle) +func (rectangle *Rectangle) Draw(img draw.Image, x, y int) error { + if err := checkBounds(img, x, y); err != nil { + return err + } + fill := rectangle.fill + x0, x1 := rectangle.Rectangle.Min.X, rectangle.Rectangle.Max.X + y0, y1 := rectangle.Rectangle.Min.Y, rectangle.Rectangle.Max.Y + x0 += x + x1 += x + y0 += y + y1 += y + if !rectangle.filled { + drawLine(img, image.Point{x0, y0}, image.Point{x1, y0}, fill) + drawLine(img, image.Point{x0, y1}, image.Point{x1, y1}, fill) + drawLine(img, image.Point{x0, y0}, image.Point{x0, y1}, fill) + drawLine(img, image.Point{x1, y0}, image.Point{x1, y1}, fill) + } else { + draw.Draw(img, image.Rect(x0, y0, x1+1, y1+1), + &image.Uniform{fill}, image.Pt(x0, y0), draw.Src) + } + return nil +} + +type Option struct { + Fill color.Color + Radius int + Rect image.Rectangle + Filled bool +} + +func New(shape string, option Option) (Shaper, error) { + sidesForShape := map[string]int{"triangle": 3, "square": 4, + "pentagon": 5, "hexagon": 6, "heptagon": 7, "octagon": 8, + "enneagon": 9, "nonagon": 9, "decagon": 10} + if sides, found := sidesForShape[shape]; found { + return NewRegularPolygon(option.Fill, option.Radius, sides), nil + } + if shape == "rectangle" { + rect := NewRectangle(option.Fill, option.Rect) + rect.SetFilled(option.Filled) + return rect, nil + } + if shape != "circle" { + return nil, fmt.Errorf("shapes.New(): invalid shape '%s'", shape) + } + return NewCircle(option.Fill, option.Radius), nil +} + +func FilledImage(width, height int, fill color.Color) draw.Image { + if fill == nil { + fill = color.Black + } + width = saneLength(width) + height = saneLength(height) + img := image.NewRGBA(image.Rect(0, 0, width, height)) + draw.Draw(img, img.Bounds(), &image.Uniform{fill}, image.ZP, draw.Src) + return img +} + +func DrawShapes(img draw.Image, x, y int, shapes ...Drawer) error { + for _, shape := range shapes { + if err := shape.Draw(img, x, y); err != nil { + return err + } + // Thicker so that it shows up better in screenshots + if err := shape.Draw(img, x+1, y); err != nil { + return err + } + if err := shape.Draw(img, x, y+1); err != nil { + return err + } + } + return nil +} + +func SaveImage(img image.Image, filename string) error { + file, err := os.Create(filename) + if err != nil { + return err + } + defer file.Close() + switch strings.ToLower(filepath.Ext(filename)) { + case ".jpg", ".jpeg": + return jpeg.Encode(file, img, nil) + case ".png": + return png.Encode(file, img) + } + return fmt.Errorf("shapes.SaveImage(): '%s' has an unrecognized "+ + "suffix", filename) +} diff --git a/src/shaper_ans3/shaper3.go b/src/shaper_ans3/shaper3.go new file mode 100644 index 0000000..fd5a489 --- /dev/null +++ b/src/shaper_ans3/shaper3.go @@ -0,0 +1,38 @@ +// Copyright © 2011-12 Qtrac Ltd. +// +// This program or package and any associated files are licensed under the +// Apache License, Version 2.0 (the "License"); you may not use these files +// except in compliance with the License. You can get a copy of the License +// at: http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "image" + "image/color" + "shaper_ans3/shapes" +) + +func main() { + img := shapes.FilledImage(420, 220, image.White) + fill := color.RGBA{200, 200, 200, 0xFF} // light gray + for i := 0; i < 10; i++ { + width, height := 40+(20*i), 20+(10*i) + rectangle := shapes.Rectangle{fill, + image.Rect(0, 0, width, height), true} + x := 10 + (20 * i) + for j := i / 2; j >= 0; j-- { + rectangle.Draw(img, x+j, (x/2)+j) + } + fill.R -= uint8(i * 5) + fill.G = fill.R + fill.B = fill.R + } + shapes.SaveImage(img, "rectangle.png") +} diff --git a/src/shaper_ans3/shapes/shapes.go b/src/shaper_ans3/shapes/shapes.go new file mode 100644 index 0000000..333c577 --- /dev/null +++ b/src/shaper_ans3/shapes/shapes.go @@ -0,0 +1,308 @@ +// Copyright © 2011-12 Qtrac Ltd. +// +// This program or package and any associated files are licensed under the +// Apache License, Version 2.0 (the "License"); you may not use these files +// except in compliance with the License. You can get a copy of the License +// at: http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package shapes + +import ( + "fmt" + "image" + "image/color" + "image/draw" + "image/jpeg" + "image/png" + "math" + "os" + "path/filepath" + "runtime" + "strings" +) + + +func clamp(minimum, x, maximum int) int { + switch { + case x < minimum: + return minimum + case x > maximum: + return maximum + } + return x +} + +func validFillColor(fill color.Color) color.Color { + if fill == nil { // We silently treat a nil color as black + return color.Black + } + return fill +} + +type Drawer interface { + Draw(img draw.Image, x, y int) error +} + +type Circle struct { + color.Color + Radius int +} + +func (circle Circle) Draw(img draw.Image, x, y int) error { + // Algorithm taken from + // http://en.wikipedia.org/wiki/Midpoint_circle_algorithm + // No need to check the radius is in bounds because you can only + // create circles using NewCircle() which guarantees it is within + // bounds. But the x, y might be outside the image so we check. + if err := checkBounds(img, x, y); err != nil { + return err + } + fill := validFillColor(circle.Color) + radius := clamp(1, circle.Radius, 1024) + + x0, y0 := x, y + f := 1 - radius + ddF_x, ddF_y := 1, -2*radius + x, y = 0, radius + + img.Set(x0, y0+radius, fill) + img.Set(x0, y0-radius, fill) + img.Set(x0+radius, y0, fill) + img.Set(x0-radius, y0, fill) + + for x < y { + if f >= 0 { + y-- + ddF_y += 2 + f += ddF_y + } + x++ + ddF_x += 2 + f += ddF_x + img.Set(x0+x, y0+y, fill) + img.Set(x0-x, y0+y, fill) + img.Set(x0+x, y0-y, fill) + img.Set(x0-x, y0-y, fill) + img.Set(x0+y, y0+x, fill) + img.Set(x0-y, y0+x, fill) + img.Set(x0+y, y0-x, fill) + img.Set(x0-y, y0-x, fill) + } + return nil +} + +func (circle Circle) String() string { + return fmt.Sprintf("circle(fill=%v, radius=%d)", circle.Color, + circle.Radius) +} + +func checkBounds(img image.Image, x, y int) error { + if !image.Rect(x, y, x, y).In(img.Bounds()) { + return fmt.Errorf("%s(): point (%d, %d) is outside the image\n", + caller(1), x, y) + } + return nil +} + +func caller(steps int) string { + name := "?" + if pc, _, _, ok := runtime.Caller(steps + 1); ok { + name = filepath.Base(runtime.FuncForPC(pc).Name()) + } + return name +} + +type RegularPolygon struct { + color.Color + Radius int + Sides int +} + + +func (polygon RegularPolygon) Draw(img draw.Image, x, y int) error { + // No need to check the radius or sides are in bounds because you can + // only create polygons using NewRegularPolygon() which guarantees they + // are within bounds. But the x, y might be outside the image so we + // check. len(points) == sides + 1 + if err := checkBounds(img, x, y); err != nil { + return err + } + fill := validFillColor(polygon.Color) + radius := clamp(1, polygon.Radius, 1024) + sides := clamp(3, polygon.Sides, 60) + points := getPoints(x, y, sides, float64(radius)) + for i := 0; i < sides; i++ { // Draw lines between the apexes + drawLine(img, points[i], points[i+1], fill) + } + return nil +} + +func getPoints(x, y, sides int, radius float64) []image.Point { + points := make([]image.Point, sides+1) + // Compute the shape's apexes (thanks to Jasmin Blanchette) + fullCircle := 2 * math.Pi + x0, y0 := float64(x), float64(y) + for i := 0; i < sides; i++ { + θ := float64(float64(i) * fullCircle / float64(sides)) + x1 := x0 + (radius * math.Sin(θ)) + y1 := y0 + (radius * math.Cos(θ)) + points[i] = image.Pt(int(x1), int(y1)) + } + points[sides] = points[0] // close the shape + return points +} + +// Based on my Perl Image::Base.pm module's line() method +func drawLine(img draw.Image, start, end image.Point, + fill color.Color) { + x0, x1 := start.X, end.X + y0, y1 := start.Y, end.Y + Δx := math.Abs(float64(x1 - x0)) + Δy := math.Abs(float64(y1 - y0)) + if Δx >= Δy { // shallow slope + if x0 > x1 { + x0, y0, x1, y1 = x1, y1, x0, y0 + } + y := y0 + yStep := 1 + if y0 > y1 { + yStep = -1 + } + remainder := float64(int(Δx/2)) - Δx + for x := x0; x <= x1; x++ { + img.Set(x, y, fill) + remainder += Δy + if remainder >= 0.0 { + remainder -= Δx + y += yStep + } + } + } else { // steep slope + if y0 > y1 { + x0, y0, x1, y1 = x1, y1, x0, y0 + } + x := x0 + xStep := 1 + if x0 > x1 { + xStep = -1 + } + remainder := float64(int(Δy/2)) - Δy + for y := y0; y <= y1; y++ { + img.Set(x, y, fill) + remainder += Δx + if remainder >= 0.0 { + remainder -= Δy + x += xStep + } + } + } +} + +func (polygon RegularPolygon) String() string { + return fmt.Sprintf("polygon(fill=%v, radius=%d, sides=%d)", + polygon.Color, polygon.Radius, polygon.Sides) +} + +type Rectangle struct { + color.Color + image.Rectangle + Filled bool +} + +// x, y are the top-left (for radius-based shapes they are the middle) +func (rectangle Rectangle) Draw(img draw.Image, x, y int) error { + if err := checkBounds(img, x, y); err != nil { + return err + } + fill := validFillColor(rectangle.Color) + x0 := clamp(1, rectangle.Rectangle.Min.X, 4096) + x1 := clamp(1, rectangle.Rectangle.Max.X, 4096) + y0 := clamp(1, rectangle.Rectangle.Min.Y, 4096) + y1 := clamp(1, rectangle.Rectangle.Max.Y, 4096) + x0 += x + x1 += x + y0 += y + y1 += y + if !rectangle.Filled { + drawLine(img, image.Point{x0, y0}, image.Point{x1, y0}, fill) + drawLine(img, image.Point{x0, y1}, image.Point{x1, y1}, fill) + drawLine(img, image.Point{x0, y0}, image.Point{x0, y1}, fill) + drawLine(img, image.Point{x1, y0}, image.Point{x1, y1}, fill) + } else { + draw.Draw(img, image.Rect(x0, y0, x1+1, y1+1), + &image.Uniform{fill}, image.Pt(x0, y0), draw.Src) + } + return nil +} + + +type Option struct { + Fill color.Color + Radius int + Rect image.Rectangle + Filled bool +} + +func New(shape string, option Option) (Drawer, error) { + sidesForShape := map[string]int{"triangle": 3, "square": 4, + "pentagon": 5, "hexagon": 6, "heptagon": 7, "octagon": 8, + "enneagon": 9, "nonagon": 9, "decagon": 10} + if sides, found := sidesForShape[shape]; found { + return RegularPolygon{option.Fill, option.Radius, sides}, nil + } + if shape == "rectangle" { + rect := Rectangle{option.Fill, option.Rect, option.Filled} + return rect, nil + } + if shape != "circle" { + return nil, fmt.Errorf("shapes.New(): invalid shape '%s'", shape) + } + return Circle{option.Fill, option.Radius}, nil +} + +func FilledImage(width, height int, fill color.Color) draw.Image { + fill = validFillColor(fill) + width = clamp(1, width, 4096) + height = clamp(1, height, 4096) + img := image.NewRGBA(image.Rect(0, 0, width, height)) + draw.Draw(img, img.Bounds(), &image.Uniform{fill}, image.ZP, draw.Src) + return img +} + +func DrawShapes(img draw.Image, x, y int, drawers ...Drawer) error { + for _, drawer := range drawers { + if err := drawer.Draw(img, x, y); err != nil { + return err + } + // Thicker so that it shows up better in screenshots + if err := drawer.Draw(img, x+1, y); err != nil { + return err + } + if err := drawer.Draw(img, x, y+1); err != nil { + return err + } + } + return nil +} + +func SaveImage(img image.Image, filename string) error { + file, err := os.Create(filename) + if err != nil { + return err + } + defer file.Close() + switch strings.ToLower(filepath.Ext(filename)) { + case ".jpg", ".jpeg": + return jpeg.Encode(file, img, nil) + case ".png": + return png.Encode(file, img) + } + return fmt.Errorf("shapes.SaveImage(): '%s' has an unrecognized "+ + "suffix", filename) +} diff --git a/src/sizeimages1/sizeimages1.go b/src/sizeimages1/sizeimages1.go new file mode 100644 index 0000000..9ffe8f9 --- /dev/null +++ b/src/sizeimages1/sizeimages1.go @@ -0,0 +1,171 @@ +// Copyright © 2011-12 Qtrac Ltd. +// +// This program or package and any associated files are licensed under the +// Apache License, Version 2.0 (the "License"); you may not use these files +// except in compliance with the License. You can get a copy of the License +// at: http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "fmt" + "image" + "io/ioutil" + "os" + "path/filepath" + "regexp" + "runtime" + "strings" + + _ "image/gif" + _ "image/jpeg" + _ "image/png" +) + +var workers = runtime.NumCPU() +const ( + widthAttr = "width=" + heightAttr = "height=" +) + +var ( + imageRx *regexp.Regexp + srcRx *regexp.Regexp +) + +func init() { + imageRx = regexp.MustCompile(`<[iI][mM][gG][^>]+>`) + srcRx = regexp.MustCompile(`src=["']([^"']+)["']`) +} + +func main() { + runtime.GOMAXPROCS(runtime.NumCPU()) // Use all the machine's cores + if len(os.Args) == 1 || os.Args[1] == "-h" || os.Args[1] == "--help" { + fmt.Printf("usage: %s \n", + filepath.Base(os.Args[0])) + os.Exit(1) + } + + files := commandLineFiles(os.Args[1:]) + jobs := make(chan string, workers*16) + done := make(chan struct{}, workers) + go addJobs(files, jobs) + for i := 0; i < workers; i++ { + go doJobs(done, jobs) + } + waitUntil(done) +} + +func addJobs(files []string, jobs chan<- string) { + for _, filename := range files { + suffix := strings.ToLower(filepath.Ext(filename)) + if suffix == ".html" || suffix == ".htm" { + jobs <- filename + } + } + close(jobs) +} + +func doJobs(done chan<- struct{}, jobs <-chan string) { + for job := range jobs { + sizeImages(job) + } + done <- struct{}{} +} + +func waitUntil(done <-chan struct{}) { + for i := 0; i < workers; i++ { + <-done + } +} + +func sizeImages(filename string) { + if info, err := os.Stat(filename); err != nil || + (info.Mode()&os.ModeType == 1) { + fmt.Println("ignoring:", filename) + return + } + raw, err := ioutil.ReadFile(filename) + if err != nil { + fmt.Println("failed to read:", err) + return + } + html := string(raw) // Assume ASCII or UTF-8 encoding + fmt.Println("reading", filename) + newHtml := imageRx.ReplaceAllStringFunc(html, sizer) + if len(html) != len(newHtml) { + file, err := os.Create(filename) + if err != nil { + fmt.Printf("couldn't update %s: %v\n", filename, err) + return + } + defer file.Close() + if _, err := file.WriteString(newHtml); err != nil { + fmt.Printf("error when updating %s: %v\n", filename, err) + } + } +} + +func sizer(originalTag string) string { + tag := originalTag + if strings.Index(tag, widthAttr) > -1 && + strings.Index(tag, heightAttr) > -1 { + return tag // width & height attributes are already present + } + match := srcRx.FindStringSubmatch(tag) + if match == nil { + fmt.Println("can't find 's src attribute", tag) + return tag + } + file, err := os.Open(match[1]) + if err != nil { + fmt.Println("can't open image to read its size:", err) + return tag + } + defer file.Close() + config, _, err := image.DecodeConfig(file) + if err != nil { + fmt.Println("can't ascertain the image's size:", err) + return tag + } + tag, end := tagEnd(tag) + if strings.Index(tag, widthAttr) == -1 { + tag += fmt.Sprintf(` %s"%d"`, widthAttr, config.Width) + } + if strings.Index(tag, heightAttr) == -1 { + tag += fmt.Sprintf(` %s"%d"`, heightAttr, config.Height) + } + tag += end + return tag +} + +func tagEnd(originalTag string) (tag string, end string) { + end = ">" + tag = originalTag[:len(originalTag)-1] + if tag[len(tag)-1] == '/' { + end = " />" + tag = tag[:len(tag)-1] + } + return tag, end +} + +func commandLineFiles(files []string) []string { + if runtime.GOOS == "windows" { + args := make([]string, 0, len(files)) + for _, name := range files { + if matches, err := filepath.Glob(name); err != nil { + args = append(args, name) // Invalid pattern + } else if matches != nil { // At least one match + args = append(args, matches...) + } + } + return args + } + return files +} diff --git a/src/sizeimages2/sizeimages2.go b/src/sizeimages2/sizeimages2.go new file mode 100644 index 0000000..26c845a --- /dev/null +++ b/src/sizeimages2/sizeimages2.go @@ -0,0 +1,178 @@ +// Copyright © 2011-12 Qtrac Ltd. +// +// This program or package and any associated files are licensed under the +// Apache License, Version 2.0 (the "License"); you may not use these files +// except in compliance with the License. You can get a copy of the License +// at: http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "fmt" + "image" + "io/ioutil" + "os" + "path/filepath" + "regexp" + "runtime" + "strings" + + _ "image/gif" + _ "image/jpeg" + _ "image/png" +) + +var workers = runtime.NumCPU() +const ( + widthAttr = "width=" + heightAttr = "height=" +) + +var ( + imageRx *regexp.Regexp + srcRx *regexp.Regexp +) + +func init() { + imageRx = regexp.MustCompile(`<[iI][mM][gG][^>]+>`) + srcRx = regexp.MustCompile(`src=["']([^"']+)["']`) +} + +func main() { + runtime.GOMAXPROCS(runtime.NumCPU()) // Use all the machine's cores + if len(os.Args) == 1 || os.Args[1] == "-h" || os.Args[1] == "--help" { + fmt.Printf("usage: %s \n", + filepath.Base(os.Args[0])) + os.Exit(1) + } + + files := commandLineFiles(os.Args[1:]) + jobs := make(chan string, workers*16) + done := make(chan struct{}, workers) + go addJobs(files, jobs) + for i := 0; i < workers; i++ { + go doJobs(done, jobs) + } + waitUntil(done) +} + +func addJobs(files []string, jobs chan<- string) { + for _, filename := range files { + suffix := strings.ToLower(filepath.Ext(filename)) + if suffix == ".html" || suffix == ".htm" { + jobs <- filename + } + } + close(jobs) +} + +func doJobs(done chan<- struct{}, jobs <-chan string) { + for job := range jobs { + sizeImages(job) + } + done <- struct{}{} +} + +func waitUntil(done <-chan struct{}) { + for i := 0; i < workers; i++ { + <-done + } +} + +func sizeImages(filename string) { + if info, err := os.Stat(filename); err != nil || + (info.Mode()&os.ModeType == 1) { + fmt.Println("ignoring:", filename) + return + } + raw, err := ioutil.ReadFile(filename) + if err != nil { + fmt.Println("failed to read:", err) + return + } + html := string(raw) // Assume ASCII or UTF-8 encoding + fmt.Println("reading:", filename) + dir, _ := filepath.Split(filename) + newHtml := imageRx.ReplaceAllStringFunc(html, makeSizerFunc(dir)) + if len(html) != len(newHtml) { + file, err := os.Create(filename) + if err != nil { + fmt.Printf("couldn't update %s: %v\n", filename, err) + return + } + defer file.Close() + if _, err := file.WriteString(newHtml); err != nil { + fmt.Printf("error when updating %s: %v\n", filename, err) + } + } +} + +func makeSizerFunc(dir string) func(string) string { + return func(originalTag string) string { + tag := originalTag + if strings.Contains(tag, widthAttr) && + strings.Contains(tag, heightAttr) { + return tag // width & height attributes are already present + } + match := srcRx.FindStringSubmatch(tag) + if match == nil { + fmt.Println("can't find 's src attribute", tag) + return tag + } + filename := match[1] + if !filepath.IsAbs(filename) { + filename = filepath.Join(dir, filename) + } + file, err := os.Open(filename) + if err != nil { + fmt.Println("can't open image to read its size:", err) + return tag + } + defer file.Close() + config, _, err := image.DecodeConfig(file) + if err != nil { + fmt.Println("can't ascertain the image's size:", err) + return tag + } + tag, end := tagEnd(tag) + if !strings.Contains(tag, widthAttr) { + tag += fmt.Sprintf(` %s"%d"`, widthAttr, config.Width) + } + if !strings.Contains(tag, heightAttr) { + tag += fmt.Sprintf(` %s"%d"`, heightAttr, config.Height) + } + tag += end + return tag + } +} + +func tagEnd(originalTag string) (tag string, end string) { + end = ">" + tag = originalTag[:len(originalTag)-1] + if tag[len(tag)-1] == '/' { + end = " />" + tag = tag[:len(tag)-1] + } + return strings.TrimSpace(tag), end +} + +func commandLineFiles(files []string) []string { + if runtime.GOOS == "windows" { + args := make([]string, 0, len(files)) + for _, name := range files { + if matches, err := filepath.Glob(name); err != nil { + args = append(args, name) // Invalid pattern + } else if matches != nil { // At least one match + args = append(args, matches...) + } + } + return args + } + return files +} diff --git a/src/soundex/soundex-test-data.txt b/src/soundex/soundex-test-data.txt new file mode 100644 index 0000000..23c2416 --- /dev/null +++ b/src/soundex/soundex-test-data.txt @@ -0,0 +1,25 @@ +A226 Ashcraft +A226 Ashcroft +B622 Burroughs +B620 Burrows +C532 Ciondecks +E460 Ellery +E460 Euler +E251 Example +G200 Gauss +G200 Ghosh +H416 Heilbronn +H416 Hilbert +K530 Kant +K530 Knuth +L300 Ladd +L222 Lissajous +L300 Lloyd +L222 Lukasiewicz +O600 O'Hara +R163 Robert +R150 Rubin +R163 Rupert +S532 Soundex +T522 Tymczak +W350 Wheaton diff --git a/src/soundex/soundex.go b/src/soundex/soundex.go new file mode 100644 index 0000000..baa7c24 --- /dev/null +++ b/src/soundex/soundex.go @@ -0,0 +1,154 @@ +// Copyright © 2010-12 Qtrac Ltd. +// +// This program or package and any associated files are licensed under the +// Apache License, Version 2.0 (the "License"); you may not use these files +// except in compliance with the License. You can get a copy of the License +// at: http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "fmt" + "html" + "io/ioutil" + "log" + "net/http" + "sort" + "strings" +) + +const ( + pageTop = ` + +Soundex

Soundex

+

Compute soundex codes for a list of names.

` + form = `
+
+
+ +
` + pageBottom = `` + error = `

%s

` +) + +var digitForLetter = []rune{ + 0, 1, 2, 3, 0, 1, 2, 0, 0, 2, 2, 4, 5, + // A B C D E F G H I J K L M + 5, 0, 1, 2, 6, 2, 3, 0, 1, 0, 2, 0, 2} + // N O P Q R S T U V W X Y Z + +var testCases map[string]string + +func main() { + http.HandleFunc("/", homePage) + var ok bool + if testCases, ok = populateTestCases("soundex-test-data.txt"); ok { + http.HandleFunc("/test", testPage) + } + if err := http.ListenAndServe(":9001", nil); err != nil { + log.Fatal("failed to start server", err) + } +} + +func populateTestCases(filename string) (map[string]string, bool) { + testCases := make(map[string]string) + if lines, err := ioutil.ReadFile(filename); err != nil { + log.Println(err) + return testCases, false + } else { + for _, line := range strings.Split(string(lines), "\n") { + if fields := strings.Fields(line); len(fields) == 2 { + testCases[fields[1]] = fields[0] + } + } + } + return testCases, true +} + +func homePage(writer http.ResponseWriter, request *http.Request) { + err := request.ParseForm() // Must be called before writing response + fmt.Fprint(writer, pageTop, form) + if err != nil { + fmt.Fprintf(writer, error, err) + } else { + if names := processRequest(request); len(names) > 0 { + soundexes := make([]string, len(names)) + for i, name := range names { + soundexes[i] = soundex(name) + } + fmt.Fprint(writer, formatResults(names, soundexes)) + } + } + fmt.Fprint(writer, pageBottom) +} + +func processRequest(request *http.Request) (names []string) { + if slice, found := request.Form["names"]; found && len(slice) > 0 { + text := strings.Replace(slice[0], ",", " ", -1) + names = strings.Fields(text) + } + return names +} + +// "c - 'A'" produces a 0-based index, so 'A' -> 0, 'B' -> 1, etc. +// "'0' + digitForLetter[index]" converts a one digit integer into the +// equivalent Unicode character, i.e., 0 -> "0", 1 -> "1", etc. +func soundex(name string) string { + name = strings.ToUpper(name) + chars := []rune(name) + var codes []rune + codes = append(codes, chars[0]) + for i := 1; i < len(chars); i++ { + char := chars[i] + if i > 0 && char == chars[i-1] { + continue + } + if index := char - 'A'; 0 <= index && + index < int32(len(digitForLetter)) && + digitForLetter[index] != 0 { + codes = append(codes, '0'+digitForLetter[index]) + } + } + for len(codes) < 4 { + codes = append(codes, '0') + } + return string(codes[:4]) +} + +func formatResults(names, soundexes []string) string { + text := `` + for i := range names { + text += "" + } + return text + "
NameSoundex
" + html.EscapeString(names[i]) + "" + + html.EscapeString(soundexes[i]) + "
" +} + +func testPage(writer http.ResponseWriter, request *http.Request) { + fmt.Fprint(writer, `Soundex Test + + +`) + var names []string + for name := range testCases { + names = append(names, name) + } + sort.Strings(names) + for _, name := range names { + actual := soundex(name) + expected := testCases[name] + test := `FAIL` + if actual == expected { + test = `PASS` + } + fmt.Fprintf(writer, ` +`, name, actual, expected, test) + } + fmt.Fprint(writer, "
NameSoundexExpectedTest
%s%s%s%s
") +} diff --git a/src/stacker/stack/stack.go b/src/stacker/stack/stack.go new file mode 100644 index 0000000..8f68388 --- /dev/null +++ b/src/stacker/stack/stack.go @@ -0,0 +1,51 @@ +// Copyright © 2010-12 Qtrac Ltd. +// +// This program or package and any associated files are licensed under the +// Apache License, Version 2.0 (the "License"); you may not use these files +// except in compliance with the License. You can get a copy of the License +// at: http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package stack + +import "errors" + +type Stack []interface{} + +func (stack *Stack) Pop() (interface{}, error) { + theStack := *stack + if len(theStack) == 0 { + return nil, errors.New("can't Pop() an empty stack") + } + x := theStack[len(theStack)-1] + *stack = theStack[:len(theStack)-1] + return x, nil +} + +func (stack *Stack) Push(x interface{}) { + *stack = append(*stack, x) +} + +func (stack Stack) Top() (interface{}, error) { + if len(stack) == 0 { + return nil, errors.New("can't Top() an empty stack") + } + return stack[len(stack)-1], nil +} + +func (stack Stack) Cap() int { + return cap(stack) +} + +func (stack Stack) Len() int { + return len(stack) +} + +func (stack Stack) IsEmpty() bool { + return len(stack) == 0 +} diff --git a/src/stacker/stack/stack_test.go b/src/stacker/stack/stack_test.go new file mode 100644 index 0000000..6e74b1e --- /dev/null +++ b/src/stacker/stack/stack_test.go @@ -0,0 +1,90 @@ +// Copyright © 2010-12 Qtrac Ltd. +// +// This program or package and any associated files are licensed under the +// Apache License, Version 2.0 (the "License"); you may not use these files +// except in compliance with the License. You can get a copy of the License +// at: http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package stack_test + +import ( + "stacker/stack" + "testing" +) + +func TestStack(t *testing.T) { + count := 1 + var aStack stack.Stack + assertTrue(t, aStack.Len() == 0, "expected empty Stack", count) // 1 + count++ + assertTrue(t, aStack.Cap() == 0, "expected empty Stack", count) // 2 + count++ + assertTrue(t, aStack.IsEmpty(), "expected empty Stack", count) // 3 + count++ + value, err := aStack.Pop() + assertTrue(t, value == nil, "expected nil value", count) // 4 + count++ + assertTrue(t, err != nil, "expected error", count) // 5 + count++ + value1, err := aStack.Top() + assertTrue(t, value1 == nil, "expected nil value", count) // 6 + count++ + assertTrue(t, err != nil, "expected error", count) // 7 + count++ + aStack.Push(1) + aStack.Push(2) + aStack.Push("three") + assertTrue(t, aStack.Len() == 3, "expected nonempty Stack", count) // 8 + count++ + assertTrue(t, aStack.IsEmpty() == false, "expected nonempty Stack", + count) // 9 + count++ + value2, err := aStack.Pop() + assertEqualString(t, value2.(string), "three", "unexpected text", + count) // 10 + count++ + assertTrue(t, err == nil, "no error expected", count) // 11 + count++ + value3, err := aStack.Top() + assertTrue(t, value3 == 2, "unexpected number", count) // 12 + count++ + assertTrue(t, err == nil, "no error expected", count) // 13 + count++ + aStack.Pop() + assertTrue(t, aStack.Len() == 1, "expected nonempty Stack", count) //14 + count++ + assertTrue(t, aStack.IsEmpty() == false, "expected nonempty Stack", + count) // 15 + count++ + value4, err := aStack.Pop() + assertTrue(t, value4 == 1, "unexpected number", count) // 16 + count++ + assertTrue(t, err == nil, "no error expected", count) // 17 + count++ + assertTrue(t, aStack.Len() == 0, "expected empty Stack", count) // 18 + count++ + assertTrue(t, aStack.IsEmpty(), "expected empty Stack", count) // 19 + count++ +} + +// assertTrue() calls testing.T.Error() with the given message if the +// condition is false. +func assertTrue(t *testing.T, condition bool, message string, id int) { + if !condition { + t.Errorf("#%d: %s", id, message) + } +} + +// assertEqualString() calls testing.T.Error() with the given message if +// the given strings are not equal. +func assertEqualString(t *testing.T, a, b string, message string, id int) { + if a != b { + t.Errorf("#%d: %s \"%s\" !=\n\"%s\"", id, message, a, b) + } +} diff --git a/src/stacker/stacker.go b/src/stacker/stacker.go new file mode 100644 index 0000000..225ca6c --- /dev/null +++ b/src/stacker/stacker.go @@ -0,0 +1,66 @@ +// Copyright © 2010-12 Qtrac Ltd. +// +// This program or package and any associated files are licensed under the +// Apache License, Version 2.0 (the "License"); you may not use these files +// except in compliance with the License. You can get a copy of the License +// at: http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "fmt" + "stacker/stack" + "strings" +) + +func main() { + var haystack stack.Stack + haystack.Push("hay") + haystack.Push(-15) + haystack.Push([]string{"pin", "clip", "needle"}) + haystack.Push(81.52) + for { + item, err := haystack.Pop() + if err != nil { + break + } + fmt.Println(item) + } + + var aStack stack.Stack + aStack.Push("Aarvark") + aStack.Push(5) + aStack.Push(19) + x, err := aStack.Top() + fmt.Println(x) + aStack.Push(-6e-4) + aStack.Push("Baker") + aStack.Push(-3) + aStack.Push("Cake") + aStack.Push("Dancer") + x, err = aStack.Top() + fmt.Println(x) + aStack.Push(11.7) + fmt.Println("stack is empty", aStack.IsEmpty()) + fmt.Printf("Len() == %d Cap == %d\n", aStack.Len(), aStack.Cap()) + difference := aStack.Cap() - aStack.Len() + for i := 0; i < difference; i++ { + aStack.Push(strings.Repeat("*", difference-i)) + } + fmt.Printf("Len() == %d Cap == %d\n", aStack.Len(), aStack.Cap()) + for aStack.Len() > 0 { + x, _ = aStack.Pop() + fmt.Printf("%T %v\n", x, x) + } + fmt.Println("stack is empty", aStack.IsEmpty()) + x, err = aStack.Pop() + fmt.Println(x, err) + x, err = aStack.Top() + fmt.Println(x, err) +} diff --git a/src/statistics/statistics.go b/src/statistics/statistics.go new file mode 100644 index 0000000..2694204 --- /dev/null +++ b/src/statistics/statistics.go @@ -0,0 +1,118 @@ +// Copyright © 2010-12 Qtrac Ltd. +// +// This program or package and any associated files are licensed under the +// Apache License, Version 2.0 (the "License"); you may not use these files +// except in compliance with the License. You can get a copy of the License +// at: http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "fmt" + "log" + "net/http" + "sort" + "strconv" + "strings" +) + +const ( + pageTop = ` +Statistics +

Statistics

+

Computes basic statistics for a given list of numbers

` + form = `
+
+
+ +
` + pageBottom = `` + anError = `

%s

` +) + +type statistics struct { + numbers []float64 + mean float64 + median float64 +} + +func main() { + http.HandleFunc("/", homePage) + if err := http.ListenAndServe(":9001", nil); err != nil { + log.Fatal("failed to start server", err) + } +} + +func homePage(writer http.ResponseWriter, request *http.Request) { + err := request.ParseForm() // Must be called before writing response + fmt.Fprint(writer, pageTop, form) + if err != nil { + fmt.Fprintf(writer, anError, err) + } else { + if numbers, message, ok := processRequest(request); ok { + stats := getStats(numbers) + fmt.Fprint(writer, formatStats(stats)) + } else if message != "" { + fmt.Fprintf(writer, anError, message) + } + } + fmt.Fprint(writer, pageBottom) +} + +func processRequest(request *http.Request) ([]float64, string, bool) { + var numbers []float64 + if slice, found := request.Form["numbers"]; found && len(slice) > 0 { + text := strings.Replace(slice[0], ",", " ", -1) + for _, field := range strings.Fields(text) { + if x, err := strconv.ParseFloat(field, 64); err != nil { + return numbers, "'" + field + "' is invalid", false + } else { + numbers = append(numbers, x) + } + } + } + if len(numbers) == 0 { + return numbers, "", false // no data first time form is shown + } + return numbers, "", true +} + +func formatStats(stats statistics) string { + return fmt.Sprintf(` + + + + + +
Results
Numbers%v
Count%d
Mean%f
Median%f
`, stats.numbers, len(stats.numbers), stats.mean, stats.median) +} + +func getStats(numbers []float64) (stats statistics) { + stats.numbers = numbers + sort.Float64s(stats.numbers) + stats.mean = sum(numbers) / float64(len(numbers)) + stats.median = median(numbers) + return stats +} + +func sum(numbers []float64) (total float64) { + for _, x := range numbers { + total += x + } + return total +} + +func median(numbers []float64) float64 { + middle := len(numbers) / 2 + result := numbers[middle] + if len(numbers)%2 == 0 { + result = (result + numbers[middle-1]) / 2 + } + return result +} diff --git a/src/statistics_ans/statistics.go b/src/statistics_ans/statistics.go new file mode 100644 index 0000000..14771e6 --- /dev/null +++ b/src/statistics_ans/statistics.go @@ -0,0 +1,156 @@ +// Copyright © 2010-12 Qtrac Ltd. +// +// This program or package and any associated files are licensed under the +// Apache License, Version 2.0 (the "License"); you may not use these files +// except in compliance with the License. You can get a copy of the License +// at: http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "fmt" + "log" + "math" + "net/http" + "sort" + "strconv" + "strings" +) + +const ( + pageTop = ` +Statistics +

Statistics

+

Computes basic statistics for a given list of numbers

` + form = `
+
+
+ +
` + pageBottom = `` + anError = `

%s

` +) + +type statistics struct { + numbers []float64 + mean float64 + median float64 + modes []float64 + stdDev float64 +} + +func main() { + http.HandleFunc("/", homePage) + if err := http.ListenAndServe(":9001", nil); err != nil { + log.Fatal("failed to start server", err) + } +} + +func homePage(writer http.ResponseWriter, request *http.Request) { + err := request.ParseForm() // Must be called before writing response + fmt.Fprint(writer, pageTop, form) + if err != nil { + fmt.Fprintf(writer, anError, err) + } else { + if numbers, message, ok := processRequest(request); ok { + stats := getStats(numbers) + fmt.Fprint(writer, formatStats(stats)) + } else if message != "" { + fmt.Fprintf(writer, anError, message) + } + } + fmt.Fprint(writer, pageBottom) +} + +func processRequest(request *http.Request) ([]float64, string, bool) { + var numbers []float64 + if slice, found := request.Form["numbers"]; found && len(slice) > 0 { + text := strings.Replace(slice[0], ",", " ", -1) + for _, field := range strings.Fields(text) { + if x, err := strconv.ParseFloat(field, 64); err != nil { + return numbers, "'" + field + "' is invalid", false + } else { + numbers = append(numbers, x) + } + } + } + if len(numbers) == 0 { + return numbers, "", false // no data first time form is shown + } + return numbers, "", true +} + +func formatStats(stats statistics) string { + return fmt.Sprintf(` + + + + + + + +
Results
Numbers%v
Count%d
Mean%f
Median%f
Mode%v
Std. Dev.%f
`, stats.numbers, len(stats.numbers), stats.mean, stats.median, + stats.modes, stats.stdDev) +} + +func getStats(numbers []float64) (stats statistics) { + stats.numbers = numbers + sort.Float64s(stats.numbers) + stats.mean = sum(numbers) / float64(len(numbers)) + stats.median = median(numbers) + stats.modes = mode(numbers) + stats.stdDev = stdDev(numbers, stats.mean) + return stats +} + +func sum(numbers []float64) (total float64) { + for _, x := range numbers { + total += x + } + return total +} + +func median(numbers []float64) float64 { + middle := len(numbers) / 2 + result := numbers[middle] + if len(numbers)%2 == 0 { + result = (result + numbers[middle-1]) / 2 + } + return result +} + +func mode(numbers []float64) (modes []float64) { + frequencies := make(map[float64]int, len(numbers)) + highestFrequency := 0 + for _, x := range numbers { + frequencies[x]++ + if frequencies[x] > highestFrequency { + highestFrequency = frequencies[x] + } + } + for x, frequency := range frequencies { + if frequency == highestFrequency { + modes = append(modes, x) + } + } + if highestFrequency == 1 || len(modes) == len(numbers) { + modes = modes[:0] // Or: modes = []float64{} + } + sort.Float64s(modes) + return modes +} + +func stdDev(numbers []float64, mean float64) float64 { + total := 0.0 + for _, number := range numbers { + total += math.Pow(number-mean, 2) + } + variance := total / float64(len(numbers)-1) + return math.Sqrt(variance) +} diff --git a/src/statistics_nonstop/statistics.go b/src/statistics_nonstop/statistics.go new file mode 100644 index 0000000..6597842 --- /dev/null +++ b/src/statistics_nonstop/statistics.go @@ -0,0 +1,170 @@ +// Copyright © 2011-12 Qtrac Ltd. +// +// This program or package and any associated files are licensed under the +// Apache License, Version 2.0 (the "License"); you may not use these files +// except in compliance with the License. You can get a copy of the License +// at: http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "fmt" + "log" + "math" + "net/http" + "sort" + "strconv" + "strings" +) + +const ( + pageTop = ` +Statistics +

Statistics

+

Computes basic statistics for a given list of numbers

` + form = `
+
+
+ + +
` + pageBottom = `` + anError = `

%s

` +) + +type statistics struct { + numbers []float64 + mean float64 + median float64 + modes []float64 + stdDev float64 +} + +func main() { + log.SetFlags(0) // Don't log timestamps + http.HandleFunc("/", homePage) + if err := http.ListenAndServe(":9001", nil); err != nil { + log.Fatal("failed to start server", err) + } +} + +func homePage(writer http.ResponseWriter, request *http.Request) { + defer func() { // Needed for every page + if x := recover(); x != nil { + log.Printf("[%v] caught panic: %v", request.RemoteAddr, x) + } + }() + + err := request.ParseForm() // Must be called before writing response + fmt.Fprint(writer, pageTop, form) + if err != nil { + fmt.Fprintf(writer, anError, err) + } else { + if numbers, message, ok := processRequest(request); ok { + stats := getStats(numbers) + fmt.Fprint(writer, formatStats(stats)) + log.Printf("[%v] served OK", request.RemoteAddr) + } else if message != "" { + fmt.Fprintf(writer, anError, message) + log.Printf("[%v] bad request: %v", request.RemoteAddr, + message) + } + } + fmt.Fprint(writer, pageBottom) +} + +func processRequest(request *http.Request) ([]float64, string, bool) { + if _, found := request.Form["panic"]; found { // Fake a panic + panic("user clicked panic button!") + } + var numbers []float64 + if slice, found := request.Form["numbers"]; found && len(slice) > 0 { + text := strings.Replace(slice[0], ",", " ", -1) + for _, field := range strings.Fields(text) { + if x, err := strconv.ParseFloat(field, 64); err != nil { + return numbers, "'" + field + "' is invalid", false + } else { + numbers = append(numbers, x) + } + } + } + if len(numbers) == 0 { + return numbers, "", false // no data first time form is shown + } + return numbers, "", true +} + +func formatStats(stats statistics) string { + return fmt.Sprintf(` + + + + + + + +
Results
Numbers%v
Count%d
Mean%f
Median%f
Mode%v
Std. Dev.%f
`, stats.numbers, len(stats.numbers), stats.mean, stats.median, + stats.modes, stats.stdDev) +} + +func getStats(numbers []float64) (stats statistics) { + stats.numbers = numbers + sort.Float64s(stats.numbers) + stats.mean = sum(numbers) / float64(len(numbers)) + stats.median = median(numbers) + stats.modes = mode(numbers) + stats.stdDev = stdDev(numbers, stats.mean) + return stats +} + +func sum(numbers []float64) (total float64) { + for _, x := range numbers { + total += x + } + return total +} + +func median(numbers []float64) float64 { + middle := len(numbers) / 2 + result := numbers[middle] + if len(numbers)%2 == 0 { + result = (result + numbers[middle-1]) / 2 + } + return result +} + +func mode(numbers []float64) (modes []float64) { + frequencies := make(map[float64]int, len(numbers)) + highestFrequency := 0 + for _, x := range numbers { + frequencies[x]++ + if frequencies[x] > highestFrequency { + highestFrequency = frequencies[x] + } + } + for x, frequency := range frequencies { + if frequency == highestFrequency { + modes = append(modes, x) + } + } + if highestFrequency == 1 || len(modes) == len(numbers) { + modes = modes[:0] // Or: modes = []float64{} + } + sort.Float64s(modes) + return modes +} + +func stdDev(numbers []float64, mean float64) float64 { + total := 0.0 + for _, number := range numbers { + total += math.Pow(number-mean, 2) + } + variance := total / float64(len(numbers)-1) + return math.Sqrt(variance) +} diff --git a/src/statistics_nonstop2/statistics.go b/src/statistics_nonstop2/statistics.go new file mode 100644 index 0000000..8aae96b --- /dev/null +++ b/src/statistics_nonstop2/statistics.go @@ -0,0 +1,176 @@ +// Copyright © 2011-12 Qtrac Ltd. +// +// This program or package and any associated files are licensed under the +// Apache License, Version 2.0 (the "License"); you may not use these files +// except in compliance with the License. You can get a copy of the License +// at: http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "fmt" + "log" + "math" + "net/http" + "sort" + "strconv" + "strings" +) + +const ( + pageTop = ` +Statistics +

Statistics

+

Computes basic statistics for a given list of numbers

` + form = `
+
+
+ + +
` + pageBottom = `` + anError = `

%s

` +) + +type statistics struct { + numbers []float64 + mean float64 + median float64 + modes []float64 + stdDev float64 +} + +func main() { + log.SetFlags(0) // Don't log timestamps + http.HandleFunc("/", logPanics(homePage)) + if err := http.ListenAndServe(":9001", nil); err != nil { + log.Fatal("failed to start server", err) + } +} + +func logPanics(function func(http.ResponseWriter, + *http.Request)) func(http.ResponseWriter, *http.Request) { + return func(writer http.ResponseWriter, request *http.Request) { + defer func() { + if x := recover(); x != nil { + log.Printf("[%v] caught panic: %v", request.RemoteAddr, x) + } + }() + function(writer, request) + } +} + +func homePage(writer http.ResponseWriter, request *http.Request) { + err := request.ParseForm() // Must be called before writing response + fmt.Fprint(writer, pageTop, form) + if err != nil { + fmt.Fprintf(writer, anError, err) + } else { + if numbers, message, ok := processRequest(request); ok { + stats := getStats(numbers) + fmt.Fprint(writer, formatStats(stats)) + log.Printf("[%v] served OK", request.RemoteAddr) + } else if message != "" { + fmt.Fprintf(writer, anError, message) + log.Printf("[%v] bad request: %v", request.RemoteAddr, + message) + } + } + fmt.Fprint(writer, pageBottom) +} + +func processRequest(request *http.Request) ([]float64, string, bool) { + if _, found := request.Form["panic"]; found { // Fake a panic + panic("user clicked panic button!") + } + var numbers []float64 + if slice, found := request.Form["numbers"]; found && len(slice) > 0 { + text := strings.Replace(slice[0], ",", " ", -1) + for _, field := range strings.Fields(text) { + if x, err := strconv.ParseFloat(field, 64); err != nil { + return numbers, "'" + field + "' is invalid", false + } else { + numbers = append(numbers, x) + } + } + } + if len(numbers) == 0 { + return numbers, "", false // no data first time form is shown + } + return numbers, "", true +} + +func formatStats(stats statistics) string { + return fmt.Sprintf(` + + + + + + + +
Results
Numbers%v
Count%d
Mean%f
Median%f
Mode%v
Std. Dev.%f
`, stats.numbers, len(stats.numbers), stats.mean, stats.median, + stats.modes, stats.stdDev) +} + +func getStats(numbers []float64) (stats statistics) { + stats.numbers = numbers + sort.Float64s(stats.numbers) + stats.mean = sum(numbers) / float64(len(numbers)) + stats.median = median(numbers) + stats.modes = mode(numbers) + stats.stdDev = stdDev(numbers, stats.mean) + return stats +} + +func sum(numbers []float64) (total float64) { + for _, x := range numbers { + total += x + } + return total +} + +func median(numbers []float64) float64 { + middle := len(numbers) / 2 + result := numbers[middle] + if len(numbers)%2 == 0 { + result = (result + numbers[middle-1]) / 2 + } + return result +} + +func mode(numbers []float64) (modes []float64) { + frequencies := make(map[float64]int, len(numbers)) + highestFrequency := 0 + for _, x := range numbers { + frequencies[x]++ + if frequencies[x] > highestFrequency { + highestFrequency = frequencies[x] + } + } + for x, frequency := range frequencies { + if frequency == highestFrequency { + modes = append(modes, x) + } + } + if highestFrequency == 1 || len(modes) == len(numbers) { + modes = modes[:0] // Or: modes = []float64{} + } + sort.Float64s(modes) + return modes +} + +func stdDev(numbers []float64, mean float64) float64 { + total := 0.0 + for _, number := range numbers { + total += math.Pow(number-mean, 2) + } + variance := total / float64(len(numbers)-1) + return math.Sqrt(variance) +} diff --git a/src/unpack/unpack.go b/src/unpack/unpack.go new file mode 100644 index 0000000..3cd9b98 --- /dev/null +++ b/src/unpack/unpack.go @@ -0,0 +1,174 @@ +// Copyright © 2011-12 Qtrac Ltd. +// +// This program or package and any associated files are licensed under the +// Apache License, Version 2.0 (the "License"); you may not use these files +// except in compliance with the License. You can get a copy of the License +// at: http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "archive/tar" + "archive/zip" + "compress/gzip" + "fmt" + "io" + "log" + "os" + "path/filepath" + "runtime" + "strings" +) + +func main() { + log.SetFlags(0) + if len(os.Args) == 1 || os.Args[1] == "-h" || os.Args[1] == "--help" { + fmt.Printf("usage: %s archive.{zip,tar,tar.gz}\n", + filepath.Base(os.Args[0])) + os.Exit(1) + + } + filename := os.Args[1] + if !validSuffix(filename) { + log.Fatalln("unrecognized archive suffix") + } + if err := unpackArchive(filename); err != nil { + log.Fatalln(err) + } +} + +func validSuffix(filename string) bool { + for _, suffix := range []string{".zip", ".tar", ".tar.gz"} { + if strings.HasSuffix(filename, suffix) { + return true + } + } + return false +} + +func unpackArchive(filename string) error { + if strings.HasSuffix(filename, ".zip") { + return unpackZip(filename) + } + return unpackTar(filename) +} + +func unpackZip(filename string) error { + reader, err := zip.OpenReader(filename) + if err != nil { + return err + } + defer reader.Close() + for _, zipFile := range reader.Reader.File { + name := sanitizedName(zipFile.Name) + mode := zipFile.Mode() + if mode.IsDir() { + if err = os.MkdirAll(name, 0755); err != nil { + return err + } + } else { + if err = unpackZippedFile(name, zipFile); err != nil { + return err + } + } + } + return nil +} + +func unpackZippedFile(filename string, zipFile *zip.File) error { + writer, err := os.Create(filename) + if err != nil { + return err + } + defer writer.Close() + reader, err := zipFile.Open() + if err != nil { + return err + } + defer reader.Close() + if _, err = io.Copy(writer, reader); err != nil { + return err + } + if filename == zipFile.Name { + fmt.Println(filename) + } else { + fmt.Printf("%s [%s]\n", filename, zipFile.Name) + } + return nil +} + +func unpackTar(filename string) error { + file, err := os.Open(filename) + if err != nil { + return err + } + defer file.Close() + var fileReader io.ReadCloser = file + if strings.HasSuffix(filename, ".gz") { + if fileReader, err = gzip.NewReader(file); err != nil { + return err + } + defer fileReader.Close() + } + reader := tar.NewReader(fileReader) + return unpackTarFiles(reader) +} + +func unpackTarFiles(reader *tar.Reader) error { + for { + header, err := reader.Next() + if err != nil { + if err == io.EOF { + return nil // OK + } + return err + } + filename := sanitizedName(header.Name) + switch header.Typeflag { + case tar.TypeDir: + if err = os.MkdirAll(filename, 0755); err != nil { + return err + } + case tar.TypeReg: + if err = unpackTarFile(filename, header.Name, reader); + err != nil { + return err + } + } + } + return nil +} + +func unpackTarFile(filename, tarFilename string, + reader *tar.Reader) error { + writer, err := os.Create(filename) + if err != nil { + return err + } + defer writer.Close() + if _, err = io.Copy(writer, reader); err != nil { + return err + } + if filename == tarFilename { + fmt.Println(filename) + } else { + fmt.Printf("%s [%s]\n", filename, tarFilename) + } + return nil +} + +func sanitizedName(filename string) string { + if len(filename) > 1 && filename[1] == ':' && + runtime.GOOS == "windows" { + filename = filename[2:] + } + filename = filepath.ToSlash(filename) + filename = strings.TrimLeft(filename, "/.") + return strings.Replace(filename, "../", "", -1) +} diff --git a/src/unpack_ans/unpack.go b/src/unpack_ans/unpack.go new file mode 100644 index 0000000..5d86fe4 --- /dev/null +++ b/src/unpack_ans/unpack.go @@ -0,0 +1,181 @@ +// Copyright © 2011-12 Qtrac Ltd. +// +// This program or package and any associated files are licensed under the +// Apache License, Version 2.0 (the "License"); you may not use these files +// except in compliance with the License. You can get a copy of the License +// at: http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "archive/tar" + "archive/zip" + "compress/bzip2" + "compress/gzip" + "fmt" + "io" + "log" + "os" + "path/filepath" + "strings" +) + +func main() { + log.SetFlags(0) + if len(os.Args) == 1 || os.Args[1] == "-h" || os.Args[1] == "--help" { + fmt.Printf("usage: %s archive.{zip,tar,tar.gz,tar.bz2}\n", + filepath.Base(os.Args[0])) + os.Exit(1) + + } + filename := os.Args[1] + if !validSuffix(filename) { + log.Fatalln("unrecognized archive suffix") + } + if err := unpackArchive(filename); err != nil { + log.Fatalln(err) + } +} + +func validSuffix(filename string) bool { + for _, suffix := range []string{".zip", ".tar", ".tar.gz", + ".tar.bz2"} { + if strings.HasSuffix(filename, suffix) { + return true + } + } + return false +} + +func unpackArchive(filename string) error { + if strings.HasSuffix(filename, ".zip") { + return unpackZip(filename) + } + return unpackTar(filename) +} + +func unpackZip(filename string) (err error) { + var reader *zip.ReadCloser + if reader, err = zip.OpenReader(filename); err != nil { + return err + } + defer reader.Close() + for _, zipFile := range reader.Reader.File { + filename := sanitizedName(zipFile.Name) + if strings.HasSuffix(zipFile.Name, "/") || + strings.HasSuffix(zipFile.Name, "\\") { + if err = os.MkdirAll(filename, 0755); err != nil { + return err + } + } else { + if err = unpackZippedFile(filename, zipFile); err != nil { + return err + } + } + } + return nil +} + +func unpackZippedFile(filename string, zipFile *zip.File) (err error) { + var writer *os.File + if writer, err = os.Create(filename); err != nil { + return err + } + defer writer.Close() + var reader io.ReadCloser + if reader, err = zipFile.Open(); err != nil { + return err + } + defer reader.Close() + if _, err = io.Copy(writer, reader); err != nil { + return err + } + if filename == zipFile.Name { + fmt.Println(filename) + } else { + fmt.Printf("%s [%s]\n", filename, zipFile.Name) + } + return nil +} + +func unpackTar(filename string) (err error) { + var file *os.File + if file, err = os.Open(filename); err != nil { + return err + } + defer file.Close() + var fileReader io.Reader = file + var decompressor *gzip.Reader + if strings.HasSuffix(filename, ".gz") { + if decompressor, err = gzip.NewReader(file); err != nil { + return err + } + defer decompressor.Close() + } else if strings.HasSuffix(filename, ".bz2") { + fileReader = bzip2.NewReader(file) + } + var reader *tar.Reader + if decompressor != nil { + reader = tar.NewReader(decompressor) + } else { + reader = tar.NewReader(fileReader) + } + return unpackTarFiles(reader) +} + +func unpackTarFiles(reader *tar.Reader) (err error) { + var header *tar.Header + for { + if header, err = reader.Next(); err != nil { + if err == io.EOF { + return nil // OK + } + return err + } + filename := sanitizedName(header.Name) + switch header.Typeflag { + case tar.TypeDir: + if err = os.MkdirAll(filename, 0755); err != nil { + return err + } + case tar.TypeReg: + if err = unpackTarFile(filename, header.Name, reader); err != nil { + return err + } + } + } + return nil +} + +func unpackTarFile(filename, tarFilename string, + reader *tar.Reader) (err error) { + var writer *os.File + if writer, err = os.Create(filename); err != nil { + return err + } + defer writer.Close() + if _, err = io.Copy(writer, reader); err != nil { + return err + } + if filename == tarFilename { + fmt.Println(filename) + } else { + fmt.Printf("%s [%s]\n", filename, tarFilename) + } + return nil +} + +func sanitizedName(filename string) string { + if len(filename) > 1 && filename[1] == ':' { + filename = filename[2:] + } + filename = strings.TrimLeft(filename, "\\/.") + filename = strings.Replace(filename, "../", "", -1) + return strings.Replace(filename, "..\\", "", -1) +} diff --git a/src/utf16-to-utf8/utf-16-be.txt b/src/utf16-to-utf8/utf-16-be.txt new file mode 100644 index 0000000..fda7600 Binary files /dev/null and b/src/utf16-to-utf8/utf-16-be.txt differ diff --git a/src/utf16-to-utf8/utf-16-le.txt b/src/utf16-to-utf8/utf-16-le.txt new file mode 100644 index 0000000..d51dd93 Binary files /dev/null and b/src/utf16-to-utf8/utf-16-le.txt differ diff --git a/src/utf16-to-utf8/utf16-to-utf8.go b/src/utf16-to-utf8/utf16-to-utf8.go new file mode 100644 index 0000000..3c6802d --- /dev/null +++ b/src/utf16-to-utf8/utf16-to-utf8.go @@ -0,0 +1,79 @@ +// Copyright © 2011-12 Qtrac Ltd. +// +// This program or package and any associated files are licensed under the +// Apache License, Version 2.0 (the "License"); you may not use these files +// except in compliance with the License. You can get a copy of the License +// at: http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "bufio" + "encoding/binary" + "errors" + "io" + "log" + "os" + "path/filepath" + "unicode/utf16" +) + +func main() { + log.SetFlags(0) + if len(os.Args) == 1 || os.Args[1] == "-h" || os.Args[1] == "--help" { + log.Fatalf("usage: %s utf-16-in.txt [>]utf-8-out.txt\n", + filepath.Base(os.Args[0])) + return + } + var err error + var infile *os.File + if infile, err = os.Open(os.Args[1]); err != nil { + log.Fatalln(err) + } + defer infile.Close() + outfile := os.Stdout + if len(os.Args) > 2 { + if outfile, err = os.Create(os.Args[2]); err != nil { + log.Fatalln(err) + } + defer outfile.Close() + } + if err := utf16toutf8(infile, outfile); err != nil { + log.Fatalln(err) + } +} + +func utf16toutf8(infile, outfile *os.File) error { + writer := bufio.NewWriter(outfile) + defer writer.Flush() + bom := make([]byte, 2) // Byte Order Mark + if _, err := infile.Read(bom); err != nil { + return err + } + var byteOrder binary.ByteOrder = binary.LittleEndian + if bom[0] == 0xFE && bom[1] == 0xFF { + byteOrder = binary.BigEndian + } else if bom[0] != 0xFF || bom[1] != 0xFE { + return errors.New("missing byte order mark") + } + for { + var c uint16 + if err := binary.Read(infile, byteOrder, &c); err != nil { + if err == io.EOF { + return nil + } + return err + } + if _, err := writer.WriteString( + string(utf16.Decode([]uint16{c}))); err != nil { + return err + } + } + return nil +} diff --git a/src/wordfrequency/wordfrequency.go b/src/wordfrequency/wordfrequency.go new file mode 100644 index 0000000..1500766 --- /dev/null +++ b/src/wordfrequency/wordfrequency.go @@ -0,0 +1,144 @@ +// Copyright © 2011-12 Qtrac Ltd. +// +// This program or package and any associated files are licensed under the +// Apache License, Version 2.0 (the "License"); you may not use these files +// except in compliance with the License. You can get a copy of the License +// at: http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "bufio" + "fmt" + "io" + "log" + "os" + "path/filepath" + "runtime" + "sort" + "strings" + "unicode" + "unicode/utf8" +) + +func main() { + if len(os.Args) == 1 || os.Args[1] == "-h" || os.Args[1] == "--help" { + fmt.Printf("usage: %s [ [... ]]\n", + filepath.Base(os.Args[0])) + os.Exit(1) + } + + frequencyForWord := map[string]int{} // Same as: make(map[string]int) + for _, filename := range commandLineFiles(os.Args[1:]) { + updateFrequencies(filename, frequencyForWord) + } + reportByWords(frequencyForWord) + wordsForFrequency := invertStringIntMap(frequencyForWord) + reportByFrequency(wordsForFrequency) +} + +func commandLineFiles(files []string) []string { + if runtime.GOOS == "windows" { + args := make([]string, 0, len(files)) + for _, name := range files { + if matches, err := filepath.Glob(name); err != nil { + args = append(args, name) // Invalid pattern + } else if matches != nil { // At least one match + args = append(args, matches...) + } + } + return args + } + return files +} + +func updateFrequencies(filename string, frequencyForWord map[string]int) { + var file *os.File + var err error + if file, err = os.Open(filename); err != nil { + log.Println("failed to open the file: ", err) + return + } + defer file.Close() + readAndUpdateFrequencies(bufio.NewReader(file), frequencyForWord) +} + +func readAndUpdateFrequencies(reader *bufio.Reader, + frequencyForWord map[string]int) { + for { + line, err := reader.ReadString('\n') + for _, word := range SplitOnNonLetters(strings.TrimSpace(line)) { + if len(word) > utf8.UTFMax || + utf8.RuneCountInString(word) > 1 { + frequencyForWord[strings.ToLower(word)] += 1 + } + } + if err != nil { + if err != io.EOF { + log.Println("failed to finish reading the file: ", err) + } + break + } + } +} +// We only want to count words of 2 or more letters. The cheapest way is to +// see if the word has enough bytes to represent at least one UTF-8 +// character that uses the most possible bytes (i.e., 4); but if it is less +// than 4 bytes it could still be a 2, 3, or 4 letter word if they're 7-bit +// ASCII so for this case we actually count the runes (which will be cheap +// because there are at most 4 to count) + +func SplitOnNonLetters(s string) []string { + notALetter := func(char rune) bool { return !unicode.IsLetter(char) } + return strings.FieldsFunc(s, notALetter) +} + +func invertStringIntMap(intForString map[string]int) map[int][]string { + stringsForInt := make(map[int][]string, len(intForString)) + for key, value := range intForString { + stringsForInt[value] = append(stringsForInt[value], key) + } + return stringsForInt +} + +func reportByWords(frequencyForWord map[string]int) { + words := make([]string, 0, len(frequencyForWord)) + wordWidth, frequencyWidth := 0, 0 + for word, frequency := range frequencyForWord { + words = append(words, word) + if width := utf8.RuneCountInString(word); width > wordWidth { + wordWidth = width + } + if width := len(fmt.Sprint(frequency)); width > frequencyWidth { + frequencyWidth = width + } + } + sort.Strings(words) + gap := wordWidth + frequencyWidth - len("Word") - len("Frequency") + fmt.Printf("Word %*s%s\n", gap, " ", "Frequency") + for _, word := range words { + fmt.Printf("%-*s %*d\n", wordWidth, word, frequencyWidth, + frequencyForWord[word]) + } +} + +func reportByFrequency(wordsForFrequency map[int][]string) { + frequencies := make([]int, 0, len(wordsForFrequency)) + for frequency := range wordsForFrequency { + frequencies = append(frequencies, frequency) + } + sort.Ints(frequencies) + width := len(fmt.Sprint(frequencies[len(frequencies)-1])) + fmt.Println("Frequency → Words") + for _, frequency := range frequencies { + words := wordsForFrequency[frequency] + sort.Strings(words) + fmt.Printf("%*d %s\n", width, frequency, strings.Join(words, ", ")) + } +}