├── .envrc ├── .github └── workflows │ └── golangci-lint.yml ├── .gitignore ├── .golangci.yml ├── .travis.yml ├── LICENSE ├── Makefile ├── README.md ├── examples ├── ex1 │ ├── VariantA.json │ ├── VariantA.png │ ├── VariantB.json │ ├── VariantB.png │ └── main.go ├── ex2 │ ├── VariantA.json │ ├── VariantA.png │ ├── VariantB.json │ ├── VariantB.png │ └── main.go └── ex3 │ ├── AB.png │ └── main.go ├── go.mod ├── go.sum ├── json.go ├── plot.go ├── result.go ├── runner.go ├── stepper.go ├── stepper_test.go └── util.go /.envrc: -------------------------------------------------------------------------------- 1 | PATH_add .bin -------------------------------------------------------------------------------- /.github/workflows/golangci-lint.yml: -------------------------------------------------------------------------------- 1 | # https://github.com/marketplace/actions/run-golangci-lint 2 | 3 | name: golangci-lint 4 | on: 5 | push: 6 | tags: 7 | - v* 8 | branches: 9 | - master 10 | pull_request: 11 | jobs: 12 | golangci: 13 | name: lint 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: golangci-lint 18 | uses: golangci/golangci-lint-action@v2 19 | with: 20 | # Required: the version of golangci-lint is required and must be specified without patch version: we always use the latest patch version. 21 | version: v1.43 22 | 23 | # Optional: working directory, useful for monorepos 24 | # working-directory: somedir 25 | 26 | # Optional: golangci-lint command line arguments. 27 | # args: --issues-exit-code=0 28 | 29 | # Optional: show only new issues if it's a pull request. The default value is `false`. 30 | # only-new-issues: true -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, build with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | .idea 15 | .bin -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | # https://github.com/golangci/golangci-lint#config-file 2 | 3 | issues: 4 | exclude: 5 | - Using the variable on range scope .* in function literal 6 | 7 | exclude-rules: 8 | - path: _test\.go 9 | text: .*do not define dynamic errors.* 10 | 11 | linters: 12 | enable: 13 | - govet 14 | - gofmt 15 | - goimports 16 | - errcheck 17 | - errorlint 18 | - gosec 19 | - nilerr 20 | - noctx 21 | - exportloopref 22 | - staticcheck 23 | - cyclop 24 | - gocyclo 25 | - funlen 26 | - nestif 27 | - goerr113 28 | - gocritic 29 | - bodyclose 30 | - prealloc 31 | - structcheck 32 | - wsl 33 | - makezero 34 | - deadcode 35 | - unparam 36 | - unused 37 | - varcheck 38 | fast: false -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.x 5 | 6 | os: 7 | - linux 8 | - windows 9 | 10 | env: 11 | global: 12 | - GO111MODULE=on 13 | - GOFLAGS="-mod=readonly" 14 | 15 | go_import_path: github.com/Oppodelldog/bigo 16 | 17 | script: 18 | - go test -race -covermode=atomic ./... -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Nils Wogatzky 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | export PROJECTS_ROOT := $(abspath $(shell pwd)/../) 2 | 3 | setup: ## Install tools 4 | go install golang.org/x/tools/cmd/goimports 5 | go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.43.0 6 | 7 | lint: ## Run the linters 8 | golangci-lint run 9 | 10 | tests: ## run all the tests 11 | go version 12 | go env 13 | go list ./... | xargs -n1 -I{} sh -c 'go test -race {}' 14 | 15 | fmt: ## gofmt and goimports all go files 16 | find . -name '*.go' -not -wholename './vendor/*' | while read -r file; do gofmt -w -s "$$file"; goimports -w "$$file"; done 17 | 18 | # Self-Documented Makefile see https://marmelab.com/blog/2016/02/29/auto-documented-makefile.html 19 | help: 20 | @awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST) 21 | 22 | .DEFAULT_GOAL := help -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Big-O Run & Plot 2 | > Library that helps to run Big-O Experiments and plot the output 3 | 4 | [![Go Report Card](https://goreportcard.com/badge/github.com/Oppodelldog/bigo)](https://goreportcard.com/report/github.com/Oppodelldog/bigo) 5 | [![godoc](https://img.shields.io/badge/godoc-reference-5272B4.svg)](https://godoc.org/github.com/Oppodelldog/bigo) 6 | [![License](https://img.shields.io/github/license/mashape/apistatus.svg)](https://raw.githubusercontent.com/Oppodelldog/bigo/master/LICENSE) 7 | 8 | ## Example comparing two variants 9 | [examples/ex1/main.go](examples/ex1/main.go) 10 | ```go 11 | package main 12 | 13 | import ( 14 | "time" 15 | 16 | "github.com/Oppodelldog/bigo" 17 | ) 18 | 19 | func main() { 20 | for testName, testRunner := range map[string]Runner{ 21 | "VariantA": {Sleep: 100}, 22 | "VariantB": {Sleep: 200}, 23 | } { 24 | bigo. 25 | New( 26 | testName, 27 | testRunner, 28 | bigo.NewArrayStepper([]float64{1, 2, 3}), 29 | ). 30 | Run(). 31 | WriteResultsToJson(). 32 | PlotResults() 33 | } 34 | } 35 | 36 | // Runner implements TestRunner 37 | type Runner struct { 38 | Sleep int 39 | } 40 | 41 | // Step simulated to test some logic. For simplicity it simply waits N*r.Sleep milliseconds. 42 | func (r Runner) Step(n float64) bigo.OMeasures { 43 | timeStart := time.Now() 44 | 45 | // TODO: put your code under test here 46 | time.Sleep(time.Millisecond * time.Duration(r.Sleep) * time.Duration(n)) 47 | 48 | return bigo.OMeasures{{O: float64(time.Since(timeStart).Milliseconds())}} 49 | } 50 | ``` 51 | 52 | Variant A | Variant B 53 | :-------------------------:|:-------------------------: 54 | ![](examples/ex1/VariantA.png) | ![](examples/ex1/VariantB.png) 55 | 56 | ## Example extended capturing, N-2d 57 | Let's assume you want to test every N with another subset of test values. 58 | For example **N** would represent the number of Records in your database. 59 | Now you want to test how your algorithm reacts on different user inputs. 60 | This is why **Step** returns a list of measures **bigo.OMeasures**. 61 | This allows to capture multiple Os for every **N**. 62 | The plot the will reflect that in **min, max, mean, all** 63 | 64 | **Here's a sample** 65 | [examples/ex2/main.go](examples/ex2/main.go) 66 | 67 | ```go 68 | // Step simulated 3 additional scales to the given N. In this case 69 | func (r Runner) Step(n float64) bigo.OMeasures { 70 | var measures bigo.OMeasures 71 | for i := 1; i <= 3; i++ { 72 | timeStart := time.Now() 73 | time.Sleep(time.Millisecond * time.Duration(r.Sleep) * time.Duration(n) * time.Duration(i*r.Factor)) 74 | measures = append(measures, bigo.OMeasure{O: float64(time.Since(timeStart).Milliseconds())}) 75 | } 76 | 77 | return measures 78 | } 79 | 80 | ``` 81 | 82 | Variant A | Variant B 83 | :-------------------------:|:-------------------------: 84 | ![](examples/ex2/VariantA.png) | ![](examples/ex2/VariantB.png) 85 | 86 | 87 | ## Example combining multiple Results in one plot 88 | 89 | To combine mutliple capture results in one plot you have to collect 90 | the Results into a **bigo.PlotSeriesList**, which then can be passed 91 | to **bigo.PlotTestResults** to generate one plot file. 92 | 93 | **Here's a sample** 94 | [examples/ex3/main.go](examples/ex3/main.go) 95 | 96 | ```go 97 | func main() { 98 | seriesList := bigo.PlotSeriesList{} 99 | for testName, testRunner := range map[string]Runner{ 100 | "VariantA": {Sleep: 100, Factor: 1}, 101 | "VariantB": {Sleep: 200, Factor: 2}, 102 | } { 103 | seriesList = append(seriesList, bigo.PlotSeries{Name: testName, Results: bigo. 104 | New( 105 | testName, 106 | testRunner, 107 | bigo.NewArrayStepper([]float64{1, 2, 3}), 108 | ). 109 | Run().GetResults(), 110 | }) 111 | } 112 | 113 | // plot the collected result data and create one plot out of the data 114 | bigo.PlotTestResults("A/B", seriesList) 115 | } 116 | ``` 117 | 118 | Combined Plot | 119 | :-------------------------:| 120 | ![](examples/ex3/AB.png)| -------------------------------------------------------------------------------- /examples/ex1/VariantA.json: -------------------------------------------------------------------------------- 1 | [{"N":1,"M":[{"V":null,"O":100}]},{"N":2,"M":[{"V":null,"O":200}]},{"N":3,"M":[{"V":null,"O":300}]}] -------------------------------------------------------------------------------- /examples/ex1/VariantA.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Oppodelldog/bigo/99d3c1993c4c291e78adf9985691a2ccd403b504/examples/ex1/VariantA.png -------------------------------------------------------------------------------- /examples/ex1/VariantB.json: -------------------------------------------------------------------------------- 1 | [{"N":1,"M":[{"V":null,"O":200}]},{"N":2,"M":[{"V":null,"O":400}]},{"N":3,"M":[{"V":null,"O":600}]}] -------------------------------------------------------------------------------- /examples/ex1/VariantB.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Oppodelldog/bigo/99d3c1993c4c291e78adf9985691a2ccd403b504/examples/ex1/VariantB.png -------------------------------------------------------------------------------- /examples/ex1/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/Oppodelldog/bigo" 7 | ) 8 | 9 | func main() { 10 | const ( 11 | sleepA = 100 * time.Millisecond 12 | sleepB = 200 * time.Millisecond 13 | ) 14 | 15 | for testName, testRunner := range map[string]Runner{ 16 | "VariantA": {Sleep: sleepA}, 17 | "VariantB": {Sleep: sleepB}, 18 | } { 19 | bigo. 20 | New( 21 | testName, 22 | testRunner, 23 | bigo.NewArrayStepper([]float64{1, 2, 3}), 24 | ). 25 | Run(). 26 | WriteResultsToJSON(). 27 | PlotResults() 28 | } 29 | } 30 | 31 | // Runner implements TestRunner. 32 | type Runner struct { 33 | Sleep time.Duration 34 | } 35 | 36 | // Step simulated to test some logic. For simplicity it simply waits N*r.Sleep milliseconds. 37 | func (r Runner) Step(n float64) bigo.OMeasures { 38 | timeStart := time.Now() 39 | 40 | time.Sleep(r.Sleep * time.Duration(n)) // sleep is just for simulation real logic that is being measured. 41 | 42 | return bigo.OMeasures{{O: float64(time.Since(timeStart).Milliseconds())}} 43 | } 44 | -------------------------------------------------------------------------------- /examples/ex2/VariantA.json: -------------------------------------------------------------------------------- 1 | [{"N":1,"M":[{"V":null,"O":100},{"V":null,"O":200},{"V":null,"O":300}]},{"N":2,"M":[{"V":null,"O":200},{"V":null,"O":400},{"V":null,"O":600}]},{"N":3,"M":[{"V":null,"O":300},{"V":null,"O":600},{"V":null,"O":900}]}] -------------------------------------------------------------------------------- /examples/ex2/VariantA.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Oppodelldog/bigo/99d3c1993c4c291e78adf9985691a2ccd403b504/examples/ex2/VariantA.png -------------------------------------------------------------------------------- /examples/ex2/VariantB.json: -------------------------------------------------------------------------------- 1 | [{"N":1,"M":[{"V":null,"O":400},{"V":null,"O":801},{"V":null,"O":1200}]},{"N":2,"M":[{"V":null,"O":801},{"V":null,"O":1600},{"V":null,"O":2400}]},{"N":3,"M":[{"V":null,"O":1200},{"V":null,"O":2400},{"V":null,"O":3600}]}] -------------------------------------------------------------------------------- /examples/ex2/VariantB.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Oppodelldog/bigo/99d3c1993c4c291e78adf9985691a2ccd403b504/examples/ex2/VariantB.png -------------------------------------------------------------------------------- /examples/ex2/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/Oppodelldog/bigo" 7 | ) 8 | 9 | func main() { 10 | const ( 11 | sleepA = 100 * time.Millisecond 12 | factorA = 1 13 | 14 | sleepB = 200 * time.Millisecond 15 | factorB = 2 16 | ) 17 | 18 | for testName, testRunner := range map[string]Runner{ 19 | "VariantA": {Sleep: sleepA, Factor: factorA}, 20 | "VariantB": {Sleep: sleepB, Factor: factorB}, 21 | } { 22 | bigo. 23 | New( 24 | testName, 25 | testRunner, 26 | bigo.NewArrayStepper([]float64{1, 2, 3}), 27 | ). 28 | Run(). 29 | WriteResultsToJSON(). 30 | PlotResults() 31 | } 32 | } 33 | 34 | // Runner implements TestRunner. 35 | type Runner struct { 36 | Sleep time.Duration 37 | Factor int 38 | } 39 | 40 | // Step simulated 3 additional scales to the given N. In this case. 41 | func (r Runner) Step(n float64) bigo.OMeasures { 42 | var measures bigo.OMeasures 43 | 44 | for i := 1; i <= 3; i++ { 45 | timeStart := time.Now() 46 | 47 | // sleep is just for simulation real logic that is being measured. 48 | time.Sleep(r.Sleep * time.Duration(n) * time.Duration(i*r.Factor)) 49 | 50 | measures = append(measures, bigo.OMeasure{O: float64(time.Since(timeStart).Milliseconds())}) 51 | } 52 | 53 | return measures 54 | } 55 | -------------------------------------------------------------------------------- /examples/ex3/AB.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Oppodelldog/bigo/99d3c1993c4c291e78adf9985691a2ccd403b504/examples/ex3/AB.png -------------------------------------------------------------------------------- /examples/ex3/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "time" 5 | 6 | "gonum.org/v1/plot/vg" 7 | 8 | "github.com/Oppodelldog/bigo" 9 | ) 10 | 11 | func main() { 12 | const ( 13 | sleepA = 100 * time.Millisecond 14 | factorA = 1 15 | 16 | sleepB = 200 * time.Millisecond 17 | factorB = 2 18 | ) 19 | 20 | seriesList := bigo.PlotSeriesList{} 21 | 22 | for testName, testRunner := range map[string]Runner{ 23 | "VariantA": {Sleep: sleepA, Factor: factorA}, 24 | "VariantB": {Sleep: sleepB, Factor: factorB}, 25 | } { 26 | seriesList = append(seriesList, bigo.PlotSeries{Name: testName, Results: bigo. 27 | New( 28 | testName, 29 | testRunner, 30 | bigo.NewArrayStepper([]float64{1, 2, 3}), 31 | ). 32 | Run().GetResults(), 33 | }) 34 | } 35 | 36 | const plotSize = 8 37 | 38 | plotConfig := bigo.DefaultPlotConfig 39 | plotConfig.PlotHeight = plotSize * vg.Inch 40 | plotConfig.PlotWidth = plotSize * vg.Inch 41 | // plot the collected result data and create one plot out of the data 42 | bigo.PlotTestResultsWithConfig("A/B", seriesList, plotConfig) 43 | } 44 | 45 | // Runner implements TestRunner. 46 | type Runner struct { 47 | Sleep time.Duration 48 | Factor int 49 | } 50 | 51 | // Step simulated 3 additional scales to the given N. In this case. 52 | func (r Runner) Step(n float64) bigo.OMeasures { 53 | var measures bigo.OMeasures 54 | 55 | for i := 1; i <= 3; i++ { 56 | timeStart := time.Now() 57 | 58 | // sleep is just for simulation real logic that is being measured. 59 | time.Sleep(r.Sleep * time.Duration(n) * time.Duration(i*r.Factor)) 60 | 61 | measures = append(measures, bigo.OMeasure{O: float64(time.Since(timeStart).Milliseconds())}) 62 | } 63 | 64 | return measures 65 | } 66 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/Oppodelldog/bigo 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/montanaflynn/stats v0.5.0 7 | gonum.org/v1/plot v0.10.0 8 | ) 9 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= 2 | gioui.org v0.0.0-20210308172011-57750fc8a0a6/go.mod h1:RSH6KIUZ0p2xy5zHDxgAM4zumjgTw83q2ge/PI+yyw8= 3 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= 4 | github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= 5 | github.com/ajstarks/svgo v0.0.0-20210923152817-c3b6e2f0c527 h1:NImof/JkF93OVWZY+PINgl6fPtQyF6f+hNUtZ0QZA1c= 6 | github.com/ajstarks/svgo v0.0.0-20210923152817-c3b6e2f0c527/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= 7 | github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= 8 | github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= 9 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 10 | github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= 11 | github.com/fogleman/gg v1.3.0 h1:/7zJX8F6AaYQc57WQCyN9cAIz+4bCJGO9B+dyW29am8= 12 | github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= 13 | github.com/go-fonts/dejavu v0.1.0 h1:JSajPXURYqpr+Cu8U9bt8K+XcACIHWqWrvWCKyeFmVQ= 14 | github.com/go-fonts/dejavu v0.1.0/go.mod h1:4Wt4I4OU2Nq9asgDCteaAaWZOV24E+0/Pwo0gppep4g= 15 | github.com/go-fonts/latin-modern v0.2.0 h1:5/Tv1Ek/QCr20C6ZOz15vw3g7GELYL98KWr8Hgo+3vk= 16 | github.com/go-fonts/latin-modern v0.2.0/go.mod h1:rQVLdDMK+mK1xscDwsqM5J8U2jrRa3T0ecnM9pNujks= 17 | github.com/go-fonts/liberation v0.1.1/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY= 18 | github.com/go-fonts/liberation v0.2.0 h1:jAkAWJP4S+OsrPLZM4/eC9iW7CtHy+HBXrEwZXWo5VM= 19 | github.com/go-fonts/liberation v0.2.0/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY= 20 | github.com/go-fonts/stix v0.1.0/go.mod h1:w/c1f0ldAUlJmLBvlbkvVXLAD+tAMqobIIQpmnUIzUY= 21 | github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= 22 | github.com/go-latex/latex v0.0.0-20210118124228-b3d85cf34e07/go.mod h1:CO1AlKB2CSIqUrmQPqA0gdRIlnLEY0gK5JGjh37zN5U= 23 | github.com/go-latex/latex v0.0.0-20210823091927-c0d11ff05a81 h1:6zl3BbBhdnMkpSj2YY30qV3gDcVBGtFgVsV3+/i+mKQ= 24 | github.com/go-latex/latex v0.0.0-20210823091927-c0d11ff05a81/go.mod h1:SX0U8uGpxhq9o2S/CELCSUxEWWAuoCUcVCQWv7G2OCk= 25 | github.com/go-pdf/fpdf v0.5.0 h1:GHpcYsiDV2hdo77VTOuTF9k1sN8F8IY7NjnCo9x+NPY= 26 | github.com/go-pdf/fpdf v0.5.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M= 27 | github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= 28 | github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= 29 | github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= 30 | github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= 31 | github.com/montanaflynn/stats v0.5.0 h1:2EkzeTSqBB4V4bJwWrt5gIIrZmpJBcoIRGS2kWLgzmk= 32 | github.com/montanaflynn/stats v0.5.0/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= 33 | github.com/phpdave11/gofpdf v1.4.2/go.mod h1:zpO6xFn9yxo3YLyMvW8HcKWVdbNqgIfOOp2dXMnm1mY= 34 | github.com/phpdave11/gofpdi v1.0.12/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= 35 | github.com/phpdave11/gofpdi v1.0.13/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= 36 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 37 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 38 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 39 | github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w= 40 | github.com/ruudk/golang-pdf417 v0.0.0-20201230142125-a7e3863a1245/go.mod h1:pQAZKsJ8yyVxGRWYNEm9oFB8ieLgKFnamEyDmSA0BRk= 41 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 42 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 43 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 44 | golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 45 | golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 46 | golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 47 | golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 48 | golang.org/x/exp v0.0.0-20191002040644-a1355ae1e2c3 h1:n9HxLrNxWWtEb1cA950nuEEj3QnKbtsCJ6KjcgisNUs= 49 | golang.org/x/exp v0.0.0-20191002040644-a1355ae1e2c3/go.mod h1:NOZ3BPKG0ec/BKJQgnvsSFpcKLM5xXVWnvZS97DWHgE= 50 | golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= 51 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= 52 | golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 53 | golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 54 | golang.org/x/image v0.0.0-20200119044424-58c23975cae1/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 55 | golang.org/x/image v0.0.0-20200430140353-33d19683fad8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 56 | golang.org/x/image v0.0.0-20200618115811-c13761719519/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 57 | golang.org/x/image v0.0.0-20201208152932-35266b937fa6/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 58 | golang.org/x/image v0.0.0-20210216034530-4410531fe030/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 59 | golang.org/x/image v0.0.0-20210607152325-775e3b0c77b9/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= 60 | golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d h1:RNPAfi2nHY7C2srAV8A49jpsYr0ADedCk1wq6fTMTvs= 61 | golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= 62 | golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= 63 | golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= 64 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 65 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 66 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 67 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 68 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 69 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 70 | golang.org/x/sys v0.0.0-20210304124612-50617c2ba197/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 71 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 72 | golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 73 | golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= 74 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 75 | golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 76 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 77 | golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 78 | golang.org/x/tools v0.0.0-20190927191325-030b2cf1153e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 79 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 80 | gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= 81 | gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0= 82 | gonum.org/v1/gonum v0.9.3 h1:DnoIG+QAMaF5NvxnGe/oKsgKcAc6PcUyl8q0VetfQ8s= 83 | gonum.org/v1/gonum v0.9.3/go.mod h1:TZumC3NeyVQskjXqmyWt4S3bINhy7B4eYwW69EbyX+0= 84 | gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= 85 | gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc= 86 | gonum.org/v1/plot v0.9.0/go.mod h1:3Pcqqmp6RHvJI72kgb8fThyUnav364FOsdDo2aGW5lY= 87 | gonum.org/v1/plot v0.10.0 h1:ymLukg4XJlQnYUJCp+coQq5M7BsUJFk6XQE4HPflwdw= 88 | gonum.org/v1/plot v0.10.0/go.mod h1:JWIHJ7U20drSQb/aDpTetJzfC1KlAPldJLpkSy88dvQ= 89 | rsc.io/pdf v0.1.1 h1:k1MczvYDUvJBe93bYd7wrZLLUEcLZAuF824/I4e5Xr4= 90 | rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= 91 | -------------------------------------------------------------------------------- /json.go: -------------------------------------------------------------------------------- 1 | package bigo 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | ) 7 | 8 | // WriteResultsToJSON writes the captured results to a json file prefixed with the given name. 9 | func WriteResultsToJSONFile(name string, results Results) { 10 | j, err := json.Marshal(results) 11 | panicOnError(err) 12 | 13 | err = ioutil.WriteFile(normalizeFileName(name, "json"), j, 0600) 14 | panicOnError(err) 15 | } 16 | -------------------------------------------------------------------------------- /plot.go: -------------------------------------------------------------------------------- 1 | package bigo 2 | 3 | import ( 4 | "fmt" 5 | "image/color" 6 | "math" 7 | 8 | "gonum.org/v1/plot/vg/draw" 9 | 10 | "github.com/montanaflynn/stats" 11 | "gonum.org/v1/plot" 12 | "gonum.org/v1/plot/plotter" 13 | "gonum.org/v1/plot/vg" 14 | ) 15 | 16 | // PlotSeriesList a list PlotSeries. 17 | type PlotSeriesList []PlotSeries 18 | 19 | // PlotSeries a list of result values assigned to a name. 20 | type PlotSeries struct { 21 | Name string 22 | Results Results 23 | } 24 | 25 | const defaultPlotWidth = 6 26 | const defaultPlotHeight = 6 27 | const legendThumbnailWidth = 0.5 28 | 29 | // DefaultPlotConfig is used when calling PlotTestResults. 30 | var DefaultPlotConfig = PlotConfig{ 31 | ReferencePlots: false, 32 | PlotWidth: defaultPlotWidth * vg.Inch, 33 | PlotHeight: defaultPlotHeight * vg.Inch, 34 | LegendThumbNailWidth: legendThumbnailWidth * vg.Inch, 35 | } 36 | 37 | // PlotConfig enables to configure the plot. 38 | type PlotConfig struct { 39 | ReferencePlots bool 40 | PlotWidth vg.Length 41 | PlotHeight vg.Length 42 | LegendThumbNailWidth vg.Length 43 | } 44 | 45 | // PlotTestResults plots the given results to a file prefixed with the given name. 46 | func PlotTestResults(name string, plotSeries PlotSeriesList) { 47 | PlotTestResultsWithConfig(name, plotSeries, DefaultPlotConfig) 48 | } 49 | 50 | // PlotTestResultsWithConfig allows to plot with custom configuration. 51 | //nolint:funlen 52 | func PlotTestResultsWithConfig(name string, plotSeries PlotSeriesList, plotConfig PlotConfig) { 53 | p := plot.New() 54 | 55 | p.Title.Text = name 56 | p.X.Label.Text = "N" 57 | p.Y.Label.Text = "O" 58 | 59 | pal := newPalette() 60 | 61 | var ( 62 | maxO, minO = 0.0, math.MaxFloat64 63 | maxN, minN = 0.0, math.MaxFloat64 64 | ) 65 | 66 | for _, series := range plotSeries { 67 | c := pal.Next() 68 | seriesName := series.Name 69 | 70 | var ( 71 | O stats.Float64Data 72 | ptsMin, ptsMax, ptsMean, all plotter.XYs 73 | ) 74 | 75 | for _, results := range series.Results { 76 | var max, min, mean float64 77 | 78 | n := results.N 79 | 80 | for _, result := range results.OMeasures { 81 | all = append(all, plotter.XY{X: n, Y: result.O}) 82 | O = append(O, result.O) 83 | max = mustFloat64(stats.Max(O)) 84 | mean = mustFloat64(stats.Mean(O)) 85 | min = mustFloat64(stats.Min(O)) 86 | } 87 | 88 | ptsMin = append(ptsMin, plotter.XY{X: n, Y: min}) 89 | ptsMax = append(ptsMax, plotter.XY{X: n, Y: max}) 90 | ptsMean = append(ptsMean, plotter.XY{X: n, Y: mean}) 91 | minO = math.Min(minO, min) 92 | maxO = math.Max(maxO, max) 93 | minN = math.Min(minN, n) 94 | maxN = math.Max(maxN, n) 95 | O = stats.Float64Data{} 96 | } 97 | 98 | lMin, err := plotter.NewLine(ptsMin) 99 | panicOnError(err) 100 | lMax, err := plotter.NewLine(ptsMax) 101 | panicOnError(err) 102 | lMean, err := plotter.NewLine(ptsMean) 103 | panicOnError(err) 104 | lAll, err := plotter.NewScatter(all) 105 | panicOnError(err) 106 | 107 | lMin.Color = c 108 | lMin.Dashes = []vg.Length{vg.Points(2), vg.Points(2)} //nolint:gomnd 109 | lMax.Color = c 110 | lMax.Dashes = []vg.Length{vg.Points(2), vg.Points(2)} //nolint:gomnd 111 | lMean.Color = c 112 | lMean.Dashes = []vg.Length{vg.Points(0.5), vg.Points(0.5)} //nolint:gomnd 113 | lAll.Color = c 114 | lAll.Shape = draw.PlusGlyph{} 115 | lAll.Radius = vg.Length(5) //nolint:gomnd 116 | 117 | p.Add(lMin, lMax, lMean, lAll) 118 | p.Legend.Add(fmt.Sprintf("%s min", seriesName), lMin) 119 | p.Legend.Add(fmt.Sprintf("%s max", seriesName), lMax) 120 | p.Legend.Add(fmt.Sprintf("%s mean", seriesName), lMean) 121 | p.Legend.Add(fmt.Sprintf("%s all", seriesName), lAll) 122 | panicOnError(err) 123 | } 124 | 125 | if plotConfig.ReferencePlots { 126 | addReferencePlots(minN, maxN, minO, maxO, p) 127 | } 128 | 129 | p.Legend.ThumbnailWidth = plotConfig.LegendThumbNailWidth 130 | 131 | if err := p.Save(plotConfig.PlotWidth, plotConfig.PlotHeight, normalizeFileName(name, "png")); err != nil { 132 | panic(err) 133 | } 134 | } 135 | 136 | func mustFloat64(v float64, err error) float64 { 137 | if err != nil { 138 | panic(err) 139 | } 140 | 141 | return v 142 | } 143 | 144 | func addReferencePlots(minN float64, maxN float64, minO float64, maxO float64, p *plot.Plot) { 145 | quad := plotter.NewFunction(scaledFunction(minN, maxN, minO, maxO, func(x float64) float64 { return math.Pow(x, 2) })) 146 | quad.Color = color.RGBA{B: 0, G: 0, A: 60} //nolint:gomnd 147 | quad.Width = 1 148 | 149 | log := plotter.NewFunction(scaledFunction(minN, maxN, minO, maxO, logLimited)) 150 | log.Color = color.RGBA{B: 0, G: 0, A: 60} //nolint:gomnd 151 | log.Width = 1 152 | 153 | p.Add(quad, log) 154 | 155 | p.Legend.Add("x^2", quad) 156 | p.Legend.Add("log x", log) 157 | } 158 | 159 | func scaledFunction(minN, maxN, minO, maxO float64, f func(x float64) float64) func(x float64) float64 { 160 | return func(x float64) float64 { 161 | xScaled := scaleX(minN, maxN, x) 162 | xApplied := f(xScaled) 163 | yScaled := scaleY(minO, maxO, xApplied) 164 | 165 | return yScaled 166 | } 167 | } 168 | 169 | func scaleX(minN, maxN, n float64) float64 { 170 | const h = 100 171 | 172 | n -= minN 173 | nPercent := h / (maxN / n) 174 | scaledN := h / (h * nPercent) 175 | 176 | return scaledN 177 | } 178 | 179 | func scaleY(minO, maxO float64, n float64) float64 { 180 | o := maxO / (100 * n) 181 | o += minO 182 | 183 | return o 184 | } 185 | 186 | func logLimited(x float64) float64 { 187 | l := math.Log(x) 188 | if l < 0 { 189 | l = 0 190 | } 191 | 192 | return l 193 | } 194 | 195 | func panicOnError(err error) { 196 | if err != nil { 197 | panic(err) 198 | } 199 | } 200 | 201 | func newPalette() palette { 202 | return palette{ 203 | //nolint:gomnd 204 | colors: []color.RGBA{ 205 | {R: 211, G: 69, B: 0, A: 255}, 206 | {R: 50, G: 211, B: 50, A: 255}, 207 | {R: 0, G: 191, B: 211, A: 255}, 208 | {R: 255, G: 215, B: 0, A: 255}, 209 | {R: 186, G: 85, B: 211, A: 255}, 210 | {R: 32, G: 178, B: 170, A: 255}, 211 | }, 212 | } 213 | } 214 | 215 | type palette struct { 216 | colors []color.RGBA 217 | idx int 218 | } 219 | 220 | func (p *palette) Next() color.RGBA { 221 | if p.idx >= len(p.colors) { 222 | p.idx = 0 223 | } 224 | 225 | c := p.colors[p.idx] 226 | p.idx++ 227 | 228 | return c 229 | } 230 | -------------------------------------------------------------------------------- /result.go: -------------------------------------------------------------------------------- 1 | package bigo 2 | 3 | // OMeasure is a single capture of the O value. 4 | type OMeasure struct { 5 | // ResultValue may be used to store result values of the tested logic. 6 | // It is not used by this library, it's intention is debugging: 7 | // If those are deterministic they could be used to cross check that the tested code worked as expected. 8 | ResultValue interface{} `json:"V"` 9 | // O represents the number of operations used by the tested program. 10 | // Determining this value could be from tricky to impossible. 11 | // For a per machine comparison this could also be the duration of the logic under test. 12 | O float64 `json:"O"` 13 | } 14 | 15 | // OMeasures contains multiple instances of OMeasure. 16 | type OMeasures []OMeasure 17 | 18 | // Result is used by the library to accumulate OMeasure results per N. 19 | type Result struct { 20 | N float64 `json:"N"` 21 | OMeasures OMeasures `json:"M"` 22 | } 23 | 24 | // Results contains multiple instances of Result. 25 | type Results []Result 26 | -------------------------------------------------------------------------------- /runner.go: -------------------------------------------------------------------------------- 1 | package bigo 2 | 3 | // TestRunner runs big o time complexity tests. 4 | // BigO.Run() calls the Step method for every N and expects OMeasures to be returned. 5 | type TestRunner interface { 6 | Step(n float64) OMeasures 7 | } 8 | 9 | // RunConfig gives detailed configuration to BigO. 10 | type RunConfig struct { 11 | Speed float64 12 | } 13 | 14 | // New *BigO. 15 | func New(name string, testRunner TestRunner, stepper Stepper) *BigO { 16 | return NewWithConfig(name, testRunner, stepper, RunConfig{Speed: 1}) 17 | } 18 | 19 | // New *BigO with config. 20 | func NewWithConfig(name string, testRunner TestRunner, stepper Stepper, runConfig RunConfig) *BigO { 21 | return &BigO{ 22 | Name: name, 23 | Runner: testRunner, 24 | Results: Results{}, 25 | NStepper: stepper, 26 | RunConfig: runConfig, 27 | } 28 | } 29 | 30 | // BigO holds values and methods to run tests. 31 | type BigO struct { 32 | Name string 33 | Runner TestRunner 34 | Results Results 35 | NStepper Stepper 36 | RunConfig RunConfig 37 | } 38 | 39 | // Run starts a test run, calls the given TestRunner for every N consumed from the given Stepper. 40 | func (r *BigO) Run() *BigO { 41 | for { 42 | n, ok := r.NStepper.Next() 43 | if !ok { 44 | break 45 | } 46 | 47 | results := r.Runner.Step(n / r.RunConfig.Speed) 48 | r.Results = append(r.Results, Result{ 49 | N: n, 50 | OMeasures: results, 51 | }) 52 | } 53 | 54 | return r 55 | } 56 | 57 | // WriteResultsToJSON writes the captured results to a json file. 58 | func (r *BigO) WriteResultsToJSON() *BigO { 59 | WriteResultsToJSONFile(r.Name, r.Results) 60 | 61 | return r 62 | } 63 | 64 | // PlotResults plots a graph from the captured results to a png file. 65 | func (r *BigO) PlotResults() *BigO { 66 | PlotTestResults(r.Name, PlotSeriesList{PlotSeries{Name: r.Name, Results: r.Results}}) 67 | 68 | return r 69 | } 70 | 71 | // PlotResultsWithConfig plots a graph from the captured results to a png file. 72 | func (r *BigO) PlotResultsWithConfig(plotConfig PlotConfig) *BigO { 73 | PlotTestResultsWithConfig(r.Name, PlotSeriesList{PlotSeries{Name: r.Name, Results: r.Results}}, plotConfig) 74 | return r 75 | } 76 | 77 | // GetResults returns captured results. 78 | func (r *BigO) GetResults() Results { 79 | return r.Results 80 | } 81 | -------------------------------------------------------------------------------- /stepper.go: -------------------------------------------------------------------------------- 1 | package bigo 2 | 3 | import "errors" 4 | 5 | var ( 6 | ErrInvalidStepSize = errors.New("stepSize must be greater than zero") 7 | ErrUnorderedRange = errors.New("min must be smaller than max") 8 | ErrEmptyStepsArray = errors.New("steps array must have at least one entry") 9 | ) 10 | 11 | // Stepper implements the Next method. With every call it returned the next N and true. 12 | // The boolean return parameter will be set to false when there are more steps to process. 13 | type Stepper interface { 14 | Next() (float64, bool) 15 | } 16 | 17 | // NewRangeStepper returns a Stepper that steps from min to max incremented by stepSize. 18 | func NewRangeStepper(min, max, stepSize float64) *RangeStepper { 19 | if stepSize <= 0 { 20 | panic(ErrInvalidStepSize) 21 | } 22 | 23 | if min >= max { 24 | panic(ErrUnorderedRange) 25 | } 26 | 27 | return &RangeStepper{max: max, stepSize: stepSize, current: min} 28 | } 29 | 30 | type RangeStepper struct { 31 | max float64 32 | stepSize float64 33 | current float64 34 | } 35 | 36 | func (i *RangeStepper) Next() (float64, bool) { 37 | if i.current > i.max { 38 | return 0, false 39 | } 40 | 41 | value := i.current 42 | 43 | i.current += i.stepSize 44 | 45 | return value, true 46 | } 47 | 48 | // NewArrayStepper returns a Stepper that steps from the beginning to the end of the provided array. 49 | func NewArrayStepper(steps []float64) *ArrayStepper { 50 | if len(steps) == 0 { 51 | panic(ErrEmptyStepsArray) 52 | } 53 | 54 | return &ArrayStepper{steps: steps} 55 | } 56 | 57 | type ArrayStepper struct { 58 | steps []float64 59 | current int 60 | } 61 | 62 | func (a *ArrayStepper) Next() (float64, bool) { 63 | if a.current >= len(a.steps) { 64 | return 0, false 65 | } 66 | 67 | var value = a.steps[a.current] 68 | 69 | a.current++ 70 | 71 | return value, true 72 | } 73 | -------------------------------------------------------------------------------- /stepper_test.go: -------------------------------------------------------------------------------- 1 | package bigo 2 | 3 | import ( 4 | "errors" 5 | "reflect" 6 | "testing" 7 | ) 8 | 9 | func TestRangeStepper(t *testing.T) { 10 | var ( 11 | min = -1.0 12 | max = 1.0 13 | size = 1.0 14 | want = stepSequence{ 15 | {step: -1, ok: true}, 16 | {step: 0, ok: true}, 17 | {step: 1, ok: true}, 18 | {step: 0, ok: false}, 19 | } 20 | s = NewRangeStepper(min, max, size) 21 | ) 22 | 23 | assertTestSequence(t, s, want) 24 | } 25 | 26 | func TestNewRangeStepperPanicsForStepSizeZero(t *testing.T) { 27 | want := ErrInvalidStepSize 28 | 29 | assertPanic(t, want, func() { 30 | NewRangeStepper(0, 3, 0) 31 | }) 32 | 33 | assertPanic(t, want, func() { 34 | NewRangeStepper(0, 3, -1) 35 | }) 36 | } 37 | 38 | func TestNewRangeStepperPanicsForUnorderedRange(t *testing.T) { 39 | want := ErrUnorderedRange 40 | 41 | assertPanic(t, want, func() { 42 | NewRangeStepper(3, 3, 1) 43 | }) 44 | 45 | assertPanic(t, want, func() { 46 | NewRangeStepper(4, 3, 1) 47 | }) 48 | } 49 | 50 | func TestArrayStepper(t *testing.T) { 51 | var ( 52 | steps = []float64{-1.0, 0, 1} 53 | want = stepSequence{ 54 | {step: -1, ok: true}, 55 | {step: 0, ok: true}, 56 | {step: 1, ok: true}, 57 | {step: 0, ok: false}, 58 | } 59 | s = NewArrayStepper(steps) 60 | ) 61 | 62 | assertTestSequence(t, s, want) 63 | } 64 | 65 | func TestNewArrayStepper_PanicsForEmptyInput(t *testing.T) { 66 | want := ErrEmptyStepsArray 67 | 68 | assertPanic(t, want, func() { 69 | NewArrayStepper([]float64{}) 70 | }) 71 | } 72 | 73 | func assertPanic(t *testing.T, want error, f func()) { 74 | var got error 75 | 76 | func() { 77 | defer func() { 78 | if r := recover(); r != nil { 79 | got = r.(error) 80 | } 81 | }() 82 | 83 | f() 84 | }() 85 | 86 | if !errors.Is(got, want) { 87 | t.Fatalf("want: %v, got: %v", want, got) 88 | } 89 | } 90 | 91 | func assertTestSequence(t *testing.T, s Stepper, want stepSequence) { 92 | var got stepSequence 93 | 94 | for { 95 | v, ok := s.Next() 96 | 97 | got = append(got, step{step: v, ok: ok}) 98 | 99 | if !ok { 100 | break 101 | } 102 | } 103 | 104 | if !reflect.DeepEqual(want, got) { 105 | t.Fatalf("\nwant:\n%v\ngot:\n%v\n", want, got) 106 | } 107 | } 108 | 109 | type stepSequence []step 110 | 111 | type step struct { 112 | step float64 113 | ok bool 114 | } 115 | -------------------------------------------------------------------------------- /util.go: -------------------------------------------------------------------------------- 1 | package bigo 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | ) 7 | 8 | func normalizeFileName(name, extension string) string { 9 | var re = regexp.MustCompile(`[!"§$%&/()=?\\:,'*+~;]`) 10 | normalizedName := re.ReplaceAllString(name, "") 11 | normalizedExtension := re.ReplaceAllString(extension, "") 12 | 13 | return fmt.Sprintf("%s.%s", normalizedName, normalizedExtension) 14 | } 15 | --------------------------------------------------------------------------------