├── .gitattributes ├── .github └── FUNDING.yml ├── .gitignore ├── .golangci.yml ├── LICENSE ├── README.md ├── appdir.go ├── atexit └── atexit.go ├── build.sh ├── call.go ├── check └── check.go ├── cmdline ├── cmd.go ├── cmdline.go ├── help.go ├── option.go ├── options.go ├── parse.go ├── parse_test.go ├── usage.go ├── values.go └── version.go ├── collection ├── dict │ ├── map.go │ └── more.go ├── quadtree │ ├── node.go │ ├── quadtree.go │ └── quadtree_test.go ├── redblack │ ├── node.go │ ├── tree.go │ └── tree_test.go ├── set.go └── slice │ ├── column_sort.go │ ├── column_sort_test.go │ ├── slice.go │ └── slice_test.go ├── constants.go ├── desktop ├── browser.go ├── browser_darwin.go ├── browser_linux.go └── browser_windows.go ├── errs ├── errors.go ├── errors_test.go ├── log.go └── recovery.go ├── eval ├── evaluator.go ├── evaluator_test.go ├── example_test.go ├── fixed_evaluator.go ├── fixed_function.go ├── fixed_operators.go ├── float_evaluator.go ├── float_function.go ├── float_operators.go └── operator.go ├── fatal └── fatal.go ├── formats ├── icon │ ├── icns │ │ └── encoder.go │ ├── ico │ │ └── encoder.go │ └── icon.go ├── json │ ├── http.go │ └── json.go └── xlsx │ ├── cell.go │ ├── datetime.go │ ├── ref.go │ ├── ref_test.go │ └── sheet.go ├── go.mod ├── go.sum ├── i18n ├── i18n │ └── main.go ├── localization.go ├── localization_other.go ├── localization_test.go └── localization_windows.go ├── log ├── multilog │ └── multilog.go ├── rotation │ ├── cmdline.go │ ├── options.go │ ├── rotator.go │ └── rotator_test.go └── tracelog │ └── tracelog.go ├── nil.go ├── notifier └── notifier.go ├── rate ├── interface.go ├── limiter.go └── limiter_test.go ├── softref ├── softref.go └── softref_test.go ├── taskqueue ├── taskqueue.go └── taskqueue_test.go ├── tid └── tid.go ├── txt ├── all_caps.go ├── all_caps.txt ├── capitalize.go ├── case.go ├── case_test.go ├── collapse_spaces.go ├── collapse_spaces_test.go ├── comma.go ├── digits.go ├── digits_test.go ├── duration.go ├── duration_test.go ├── emoji.go ├── natural_sort.go ├── natural_sort_test.go ├── normalize.go ├── roman.go ├── roman_test.go ├── rune_reader.go ├── slices.go ├── substr.go ├── substr_test.go ├── truthy.go ├── unicode.go ├── unquote.go ├── vowel.go ├── wrap.go └── wrap_test.go ├── user.go ├── vcs └── git │ └── repo.go ├── xcrypto └── stream.go ├── xio ├── bom_stripper.go ├── byte_buffer.go ├── close.go ├── context.go ├── fs │ ├── copy.go │ ├── dir.go │ ├── exists.go │ ├── filename.go │ ├── internal │ │ └── temp.go │ ├── json.go │ ├── json_test.go │ ├── move.go │ ├── paths │ │ ├── paths.go │ │ ├── paths_darwin.go │ │ ├── paths_linux.go │ │ └── paths_windows.go │ ├── readable.go │ ├── roots.go │ ├── safe │ │ ├── file.go │ │ ├── file_test.go │ │ └── writefile.go │ ├── split.go │ ├── split_test.go │ ├── split_windows_test.go │ ├── tar │ │ └── untar.go │ ├── temp.go │ ├── walk.go │ ├── yaml.go │ ├── yaml_test.go │ └── zip │ │ └── unzip.go ├── linewriter.go ├── linewriter_test.go ├── network │ ├── addresses.go │ ├── external_ip.go │ ├── natpmp │ │ └── natpmp.go │ └── xhttp │ │ ├── basic_auth.go │ │ ├── server.go │ │ ├── status_response_writer.go │ │ └── utility.go ├── retrieve.go ├── tee.go └── term │ ├── ansi.go │ ├── getch.go │ ├── size_other.go │ ├── size_unix.go │ ├── terminal.go │ ├── terminal_bsd.go │ ├── terminal_linux.go │ ├── terminal_other.go │ └── terminal_unix.go └── xmath ├── bitset.go ├── bitset_test.go ├── constants.go ├── crc └── crc.go ├── fixed ├── config.go ├── errors.go ├── f128 │ ├── f128.go │ ├── f128_test.go │ ├── fraction.go │ └── fraction_test.go └── f64 │ ├── f64.go │ ├── f64_test.go │ ├── fraction.go │ └── fraction_test.go ├── geom ├── insets.go ├── line.go ├── matrix.go ├── point.go ├── poly │ ├── README.md │ ├── contour.go │ ├── edge_node.go │ ├── intersection.go │ ├── local_minima_table.go │ ├── polygon.go │ ├── polygon_node.go │ ├── polygon_test.go │ ├── scan_beam_tree.go │ ├── shapes.go │ └── vertex_type.go ├── rect.go ├── size.go └── visibility │ ├── README.md │ ├── array.go │ ├── endpoint.go │ ├── segment.go │ └── visibility.go ├── hashhelper └── hashhelper.go ├── math.go ├── num ├── benchmarks_test.go ├── int128.go ├── int128_test.go ├── uint128.go └── uint128_test.go └── rand └── random.go /.gitattributes: -------------------------------------------------------------------------------- 1 | # Set the default behavior, in case people don't have core.autocrlf set. 2 | * text=auto 3 | 4 | # Force text files to have LF 5 | *.bat text eol=crlf 6 | *.go text eol=lf 7 | *.md text eol=lf 8 | *.sh text eol=lf 9 | *.tmpl text eol=lf 10 | *.txt text eol=lf 11 | *.yaml text eol=lf 12 | *.yml text eol=lf 13 | 14 | # Denote all files that are truly binary and should not be modified. 15 | *.gif binary 16 | *.jpg binary 17 | *.jpeg binary 18 | *.otf binary 19 | *.png binary 20 | *.psd binary 21 | *.ttf binary 22 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [richardwilkes] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /tools -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Go Reference](https://pkg.go.dev/badge/github.com/richardwilkes/toolbox.svg)](https://pkg.go.dev/github.com/richardwilkes/toolbox) 2 | [![Go Report Card](https://goreportcard.com/badge/github.com/richardwilkes/toolbox)](https://goreportcard.com/report/github.com/richardwilkes/toolbox) 3 | 4 | # toolbox 5 | Toolbox for Go. 6 | 7 | > NOTE: This library already had a v1.x.y when Go modules were first introduced. Due to this, it doesn't follow the 8 | > normal convention and instead treats its releases as if they are of the v0.x.y variety (i.e. it could introduce 9 | > breaking API changes). Keep this in mind when deciding whether or not to use it. 10 | -------------------------------------------------------------------------------- /appdir.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | package toolbox 11 | 12 | import ( 13 | "os" 14 | "path/filepath" 15 | "runtime" 16 | "strings" 17 | 18 | "github.com/richardwilkes/toolbox/errs" 19 | ) 20 | 21 | // AppDir returns the logical directory the application resides within. For macOS, this means the directory where the 22 | // .app bundle resides, not the binary that's tucked down inside it. 23 | func AppDir() (string, error) { 24 | path, err := os.Executable() 25 | if err != nil { 26 | return "", errs.Wrap(err) 27 | } 28 | if path, err = filepath.EvalSymlinks(path); err != nil { 29 | return "", errs.Wrap(err) 30 | } 31 | if path, err = filepath.Abs(path); err != nil { 32 | return "", errs.Wrap(err) 33 | } 34 | path = filepath.Dir(path) 35 | if runtime.GOOS == MacOS { 36 | // Account for macOS bundles 37 | if i := strings.LastIndex(path, ".app/"); i != -1 { 38 | path = filepath.Dir(path[:i]) 39 | } 40 | } 41 | return path, nil 42 | } 43 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | set -eo pipefail 3 | 4 | trap 'echo -e "\033[33;5mBuild failed on build.sh:$LINENO\033[0m"' ERR 5 | 6 | for arg in "$@"; do 7 | case "$arg" in 8 | --all | -a) 9 | LINT=1 10 | TEST=1 11 | RACE=-race 12 | ;; 13 | --lint | -l) 14 | LINT=1 15 | ;; 16 | --race | -r) 17 | TEST=1 18 | RACE=-race 19 | ;; 20 | --test | -t) 21 | TEST=1 22 | ;; 23 | --help | -h) 24 | echo "$0 [options]" 25 | echo " -a, --all Equivalent to --lint --test --race" 26 | echo " -l, --lint Run the linters" 27 | echo " -r, --race Run the tests with race-checking enabled" 28 | echo " -t, --test Run the tests" 29 | echo " -h, --help This help text" 30 | exit 0 31 | ;; 32 | *) 33 | echo "Invalid argument: $arg" 34 | exit 1 35 | ;; 36 | esac 37 | done 38 | 39 | # Build the code 40 | echo -e "\033[33mBuilding...\033[0m" 41 | go build -v ./... 42 | 43 | # Run the linters 44 | if [ "$LINT"x == "1x" ]; then 45 | GOLANGCI_LINT_VERSION=$(curl --head -s https://github.com/golangci/golangci-lint/releases/latest | grep -i location: | sed 's/^.*v//' | tr -d '\r\n') 46 | TOOLS_DIR=$(go env GOPATH)/bin 47 | if [ ! -e "$TOOLS_DIR/golangci-lint" ] || [ "$("$TOOLS_DIR/golangci-lint" version 2>&1 | awk '{ print $4 }' || true)x" != "${GOLANGCI_LINT_VERSION}x" ]; then 48 | echo -e "\033[33mInstalling version $GOLANGCI_LINT_VERSION of golangci-lint into $TOOLS_DIR...\033[0m" 49 | mkdir -p "$TOOLS_DIR" 50 | curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b "$TOOLS_DIR" v$GOLANGCI_LINT_VERSION 51 | fi 52 | echo -e "\033[33mLinting...\033[0m" 53 | "$TOOLS_DIR/golangci-lint" run 54 | fi 55 | 56 | # Run the tests 57 | if [ "$TEST"x == "1x" ]; then 58 | if [ -n "$RACE" ]; then 59 | echo -e "\033[33mTesting with -race enabled...\033[0m" 60 | else 61 | echo -e "\033[33mTesting...\033[0m" 62 | fi 63 | go test $RACE ./... | grep -v "no test files" 64 | fi 65 | 66 | # Install executables 67 | echo -e "\033[33mInstalling executables...\033[0m" 68 | go install -v ./i18n/i18n 69 | -------------------------------------------------------------------------------- /call.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | package toolbox 11 | 12 | import ( 13 | "github.com/richardwilkes/toolbox/errs" 14 | ) 15 | 16 | // Call the provided function, safely wrapped in a errs.Recovery() handler that logs any errors via slog. 17 | func Call(f func()) { 18 | CallWithHandler(f, func(err error) { errs.Log(err) }) 19 | } 20 | 21 | // CallWithHandler calls the provided function, safely wrapped in a errs.Recovery() handler. 22 | func CallWithHandler(f func(), errHandler func(err error)) { 23 | defer errs.Recovery(errHandler) 24 | f() 25 | } 26 | -------------------------------------------------------------------------------- /cmdline/cmd.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | package cmdline 11 | 12 | import ( 13 | "github.com/richardwilkes/toolbox/errs" 14 | "github.com/richardwilkes/toolbox/i18n" 15 | ) 16 | 17 | // Cmd represents a sub-command available on the command-line. 18 | type Cmd interface { 19 | // Name should return the name of the command as it needs to be entered on the command line. 20 | Name() string 21 | // Usage should return a description of what the command does. 22 | Usage() string 23 | // Run will be called to run the command. It will be passed a fresh command line created from the command line that 24 | // was parsed to determine this command would be run, along with the arguments that have not yet been consumed. The 25 | // first argument, much like an application called from main, will be the name of the command. 26 | Run(cmdLine *CmdLine, args []string) error 27 | } 28 | 29 | func (cl *CmdLine) newWithCmd(cmd Cmd) *CmdLine { 30 | cmdLine := New(false) 31 | cmdLine.out = cl.out 32 | cmdLine.parent = cl 33 | cmdLine.cmd = cmd 34 | return cmdLine 35 | } 36 | 37 | // AddCommand adds a command to the available commands. 38 | func (cl *CmdLine) AddCommand(cmd Cmd) { 39 | if len(cl.cmds) == 0 { 40 | hc := &helpCmd{} 41 | cl.cmds[hc.Name()] = hc 42 | } 43 | cl.cmds[cmd.Name()] = cmd 44 | } 45 | 46 | // RunCommand attempts to run a command. Pass in the command line arguments, usually the result from calling Parse(). 47 | func (cl *CmdLine) RunCommand(args []string) error { 48 | if len(args) < 1 { 49 | return errs.New(i18n.Text("Must specify a command name")) 50 | } 51 | cmd := cl.cmds[args[0]] 52 | if cmd == nil { 53 | return errs.Newf(i18n.Text("'%[1]s' is not a valid %[2]s command"), args[0], AppCmdName) 54 | } 55 | return cmd.Run(cl.newWithCmd(cmd), args[1:]) 56 | } 57 | -------------------------------------------------------------------------------- /cmdline/help.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | package cmdline 11 | 12 | import ( 13 | "fmt" 14 | 15 | "github.com/richardwilkes/toolbox/atexit" 16 | "github.com/richardwilkes/toolbox/i18n" 17 | ) 18 | 19 | type helpCmd struct{} 20 | 21 | // Name implements the Cmd interface. 22 | func (c *helpCmd) Name() string { 23 | return "help" 24 | } 25 | 26 | // Usage implements the Cmd interface. 27 | func (c *helpCmd) Usage() string { 28 | return i18n.Text("Display help information for a command and exit.") 29 | } 30 | 31 | // Run implements the Cmd interface. 32 | func (c *helpCmd) Run(cmdLine *CmdLine, args []string) error { 33 | cmdLine = cmdLine.parent 34 | if len(args) > 0 { 35 | if args[0] != "help" { 36 | const helpFlag = "-h" 37 | if helpFlag != args[0] { 38 | for name, cmd := range cmdLine.cmds { 39 | if name == args[0] { 40 | return cmd.Run(cmdLine.newWithCmd(cmd), []string{helpFlag}) 41 | } 42 | } 43 | } 44 | fmt.Fprintf(cmdLine, i18n.Text("'%[1]s' is not a valid %[2]s command\n"), args[0], AppCmdName) 45 | } 46 | } 47 | cmdLine.DisplayUsage() 48 | atexit.Exit(1) 49 | return nil 50 | } 51 | -------------------------------------------------------------------------------- /cmdline/option.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | package cmdline 11 | 12 | import ( 13 | "fmt" 14 | 15 | "github.com/richardwilkes/toolbox/atexit" 16 | "github.com/richardwilkes/toolbox/errs" 17 | "github.com/richardwilkes/toolbox/i18n" 18 | ) 19 | 20 | // Value represents a value that can be set for an Option. 21 | type Value interface { 22 | // Set the contents of this value. 23 | Set(value string) error 24 | // String implements the fmt.Stringer interface. 25 | String() string 26 | } 27 | 28 | // Option represents an option available on the command line. 29 | type Option struct { 30 | value Value 31 | name string 32 | usage string 33 | arg string 34 | def string 35 | single rune 36 | } 37 | 38 | func (op *Option) isValid() (bool, error) { 39 | if op.value == nil { 40 | return false, errs.New(i18n.Text("Option must have a value")) 41 | } 42 | if op.single == 0 && op.name == "" { 43 | return false, errs.New(i18n.Text("Option must be named")) 44 | } 45 | return true, nil 46 | } 47 | 48 | func (op *Option) isBool() bool { 49 | if generalValue, ok := op.value.(*GeneralValue); ok { 50 | if _, ok = generalValue.Value.(*bool); ok { 51 | return true 52 | } 53 | } 54 | return false 55 | } 56 | 57 | // SetName sets the name for this option. Returns self for easy chaining. 58 | func (op *Option) SetName(name string) *Option { 59 | if len(name) > 1 { 60 | op.name = name 61 | } else { 62 | fmt.Println("Name must be 2+ characters") 63 | atexit.Exit(1) 64 | } 65 | return op 66 | } 67 | 68 | // SetSingle sets the single character name for this option. Returns self for easy chaining. 69 | func (op *Option) SetSingle(ch rune) *Option { 70 | op.single = ch 71 | return op 72 | } 73 | 74 | // SetArg sets the argument name for this option. Returns self for easy chaining. 75 | func (op *Option) SetArg(name string) *Option { 76 | op.arg = name 77 | return op 78 | } 79 | 80 | // SetDefault sets the default value for this option. Returns self for easy chaining. 81 | func (op *Option) SetDefault(def string) *Option { 82 | op.def = def 83 | return op 84 | } 85 | 86 | // SetUsage sets the usage message for this option. Returns self for easy chaining. 87 | func (op *Option) SetUsage(usage string) *Option { 88 | op.usage = usage 89 | return op 90 | } 91 | -------------------------------------------------------------------------------- /cmdline/options.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | package cmdline 11 | 12 | import ( 13 | "unicode" 14 | ) 15 | 16 | // Options represents a set of options. 17 | type Options []*Option 18 | 19 | // Len implements the sort.Interface interface. 20 | func (op Options) Len() int { 21 | return len(op) 22 | } 23 | 24 | // Less implements the sort.Interface interface. 25 | func (op Options) Less(i, j int) bool { 26 | in := op[i].single 27 | jn := op[j].single 28 | if in == 0 && jn != 0 { 29 | return false 30 | } 31 | if in != 0 && jn == 0 { 32 | return true 33 | } 34 | in = swapCase(in) 35 | jn = swapCase(jn) 36 | if in < jn { 37 | return true 38 | } 39 | if in == jn { 40 | return op[i].name < op[j].name 41 | } 42 | return false 43 | } 44 | 45 | // Swap implements the sort.Interface interface. 46 | func (op Options) Swap(i, j int) { 47 | op[i], op[j] = op[j], op[i] 48 | } 49 | 50 | func swapCase(ch rune) rune { 51 | if unicode.IsUpper(ch) { 52 | return unicode.ToLower(ch) 53 | } 54 | if unicode.IsLower(ch) { 55 | return unicode.ToUpper(ch) 56 | } 57 | return ch 58 | } 59 | -------------------------------------------------------------------------------- /cmdline/parse.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | package cmdline 11 | 12 | import ( 13 | "fmt" 14 | 15 | "github.com/richardwilkes/toolbox/errs" 16 | ) 17 | 18 | // Parse a command line string into its component parts. 19 | func Parse(command string) ([]string, error) { 20 | var args []string 21 | var current []rune 22 | var lookingForQuote rune 23 | var escapeNext bool 24 | for _, ch := range command { 25 | switch { 26 | case escapeNext: 27 | current = append(current, ch) 28 | escapeNext = false 29 | case lookingForQuote == ch: 30 | args = append(args, string(current)) 31 | current = nil 32 | lookingForQuote = 0 33 | case lookingForQuote != 0: 34 | current = append(current, ch) 35 | case ch == '\\': 36 | escapeNext = true 37 | case ch == '"' || ch == '\'': 38 | lookingForQuote = ch 39 | case ch == ' ' || ch == '\t': 40 | if len(current) != 0 { 41 | args = append(args, string(current)) 42 | current = nil 43 | } 44 | default: 45 | current = append(current, ch) 46 | } 47 | } 48 | if lookingForQuote != 0 { 49 | return nil, errs.New(fmt.Sprintf("unclosed quote in command line:\n%s", command)) 50 | } 51 | if len(current) != 0 { 52 | args = append(args, string(current)) 53 | } 54 | return args, nil 55 | } 56 | -------------------------------------------------------------------------------- /cmdline/parse_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | package cmdline_test 11 | 12 | import ( 13 | "testing" 14 | 15 | "github.com/richardwilkes/toolbox/check" 16 | "github.com/richardwilkes/toolbox/cmdline" 17 | ) 18 | 19 | func TestParseCommandLine(t *testing.T) { 20 | tests := []struct { 21 | input string 22 | expected []string 23 | }{ 24 | {"hello world", []string{"hello", "world"}}, 25 | {`hello "world hello"`, []string{"hello", "world hello"}}, 26 | {`'hello again' "world hello"`, []string{"hello again", "world hello"}}, 27 | {`\"hello\ world\"`, []string{`"hello world"`}}, 28 | {"hello 世界", []string{"hello", "世界"}}, 29 | {`hello\ world`, []string{"hello world"}}, 30 | } 31 | for i, one := range tests { 32 | parts, err := cmdline.Parse(one.input) 33 | check.NoError(t, err, i) 34 | check.Equal(t, one.expected, parts, i) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /cmdline/version.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | package cmdline 11 | 12 | // ShortVersion returns the app version. 13 | func ShortVersion() string { 14 | if VCSModified { 15 | return AppVersion + "~" 16 | } 17 | return AppVersion 18 | } 19 | 20 | // LongVersion returns a combination of the app version and the build number. 21 | func LongVersion() string { 22 | version := AppVersion 23 | if BuildNumber != "" { 24 | version += "-" + BuildNumber 25 | } 26 | if VCSModified { 27 | return version + "~" 28 | } 29 | return version 30 | } 31 | -------------------------------------------------------------------------------- /collection/dict/map.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | package dict 11 | 12 | // Functions in this package are only here because the Go team decided not to bring them over in the Go 1.21 maps 13 | // package when they migrated the existing code from golang.org/x/exp/maps. Why, I'm not sure, since these can be 14 | // useful. 15 | // 16 | // I chose not to use the package name "maps" to avoid collisions with the standard library. Unlike with the "slices" 17 | // package, though, I couldn't use "map", since that's a keyword. 18 | 19 | // Keys returns the keys of the map m. The keys will be in an indeterminate order. 20 | func Keys[M ~map[K]V, K comparable, V any](m M) []K { 21 | r := make([]K, 0, len(m)) 22 | for k := range m { 23 | r = append(r, k) 24 | } 25 | return r 26 | } 27 | 28 | // Values returns the values of the map m. The values will be in an indeterminate order. 29 | func Values[M ~map[K]V, K comparable, V any](m M) []V { 30 | r := make([]V, 0, len(m)) 31 | for _, v := range m { 32 | r = append(r, v) 33 | } 34 | return r 35 | } 36 | -------------------------------------------------------------------------------- /collection/dict/more.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | package dict 11 | 12 | // MapByKey returns a map of the values in 'data' keyed by the result of keyFunc. If there are duplicate keys, the last 13 | // value in data with that key will be the one in the map. 14 | func MapByKey[T any, K comparable](data []T, keyFunc func(T) K) map[K]T { 15 | m := make(map[K]T) 16 | for _, v := range data { 17 | m[keyFunc(v)] = v 18 | } 19 | return m 20 | } 21 | 22 | // MapOfSlicesByKey returns a map of the values in 'data' keyed by the result of keyFunc. Duplicate keys will have their 23 | // values appended to a slice in the map. 24 | func MapOfSlicesByKey[T any, K comparable](data []T, keyFunc func(T) K) map[K][]T { 25 | m := make(map[K][]T) 26 | for _, v := range data { 27 | k := keyFunc(v) 28 | m[k] = append(m[k], v) 29 | } 30 | return m 31 | } 32 | -------------------------------------------------------------------------------- /collection/set.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | package collection 11 | 12 | // Set holds a set of values. 13 | type Set[T comparable] map[T]struct{} 14 | 15 | // NewSet creates a new set from its input values. 16 | func NewSet[T comparable](values ...T) Set[T] { 17 | s := make(Set[T], len(values)) 18 | s.Add(values...) 19 | return s 20 | } 21 | 22 | // Len returns the number of values in the set. 23 | func (s Set[T]) Len() int { 24 | return len(s) 25 | } 26 | 27 | // Empty returns true if there are no values in the set. 28 | func (s Set[T]) Empty() bool { 29 | return len(s) == 0 30 | } 31 | 32 | // Clear the set. 33 | func (s *Set[T]) Clear() { 34 | *s = make(Set[T]) 35 | } 36 | 37 | // Add values to the set. 38 | func (s Set[T]) Add(values ...T) { 39 | for _, v := range values { 40 | s[v] = struct{}{} 41 | } 42 | } 43 | 44 | // Contains returns true if the value exists within the set. 45 | func (s Set[T]) Contains(value T) bool { 46 | _, ok := s[value] 47 | return ok 48 | } 49 | 50 | // Clone returns a copy of the set. 51 | func (s Set[T]) Clone() Set[T] { 52 | if s == nil { 53 | return nil 54 | } 55 | clone := make(Set[T], len(s)) 56 | for value := range s { 57 | clone[value] = struct{}{} 58 | } 59 | return clone 60 | } 61 | 62 | // Values returns all values in the set. 63 | func (s Set[T]) Values() []T { 64 | values := make([]T, 0, len(s)) 65 | for v := range s { 66 | values = append(values, v) 67 | } 68 | return values 69 | } 70 | -------------------------------------------------------------------------------- /collection/slice/column_sort.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | package slice 11 | 12 | import "slices" 13 | 14 | // ColumnSort sorts the slice in place using the provided comparison function. The resulting order will be as if the 15 | // slice was divided into columns and each column was sorted independently. If the slice is not evenly divisible by 16 | // the number of columns, the extra elements will be distributed across the columns from left to right. 17 | func ColumnSort[S ~[]E, E any](s S, columns int, cmp func(a, b E) int) { 18 | slices.SortFunc(s, cmp) 19 | if columns > 1 && len(s) > columns { 20 | replacement := make([]E, len(s)) 21 | step := len(s) / columns 22 | extra := len(s) - step*columns 23 | i := 0 24 | j := 0 25 | k := 1 26 | for i < len(s) { 27 | for c := range columns { 28 | replacement[i] = s[j] 29 | i++ 30 | if i >= len(s) { 31 | break 32 | } 33 | j += step 34 | if extra > c { 35 | j++ 36 | } 37 | if j >= len(s) { 38 | j = k 39 | k++ 40 | } 41 | } 42 | } 43 | copy(s, replacement) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /collection/slice/column_sort_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | package slice_test 11 | 12 | import ( 13 | "cmp" 14 | "testing" 15 | 16 | "github.com/richardwilkes/toolbox/check" 17 | "github.com/richardwilkes/toolbox/collection/slice" 18 | ) 19 | 20 | func TestColumnSort(t *testing.T) { 21 | s := []int{0, 1, 2, 3, 4, 5, 6} 22 | slice.ColumnSort(s, 2, cmp.Compare) 23 | // 0 4 24 | // 1 5 25 | // 2 6 26 | // 3 27 | check.Equal(t, []int{0, 4, 1, 5, 2, 6, 3}, s) 28 | 29 | slice.ColumnSort(s, 3, cmp.Compare) 30 | // 0 3 5 31 | // 1 4 6 32 | // 2 33 | check.Equal(t, []int{0, 3, 5, 1, 4, 6, 2}, s) 34 | 35 | s = []int{0, 1, 2, 3, 4, 5} 36 | slice.ColumnSort(s, 2, cmp.Compare) 37 | // 0 3 38 | // 1 4 39 | // 2 5 40 | check.Equal(t, []int{0, 3, 1, 4, 2, 5}, s) 41 | 42 | slice.ColumnSort(s, 4, cmp.Compare) 43 | // 0 2 4 5 44 | // 1 3 45 | check.Equal(t, []int{0, 2, 4, 5, 1, 3}, s) 46 | 47 | slice.ColumnSort(s, 10, cmp.Compare) 48 | // 0 1 2 3 4 5 49 | check.Equal(t, []int{0, 1, 2, 3, 4, 5}, s) 50 | 51 | s = []int{0, 1, 2, 3, 4, 5, 6, 7} 52 | slice.ColumnSort(s, 3, cmp.Compare) 53 | // 0 3 6 54 | // 1 4 7 55 | // 2 5 56 | check.Equal(t, []int{0, 3, 6, 1, 4, 7, 2, 5}, s) 57 | } 58 | -------------------------------------------------------------------------------- /collection/slice/slice.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | package slice 11 | 12 | import "slices" 13 | 14 | // ZeroedDelete removes the elements s[i:j] from s, returning the modified slice. This function panics if s[i:j] is not 15 | // a valid slice of s. This function modifies the contents of the slice s; it does not create a new slice. The elements 16 | // that are removed are zeroed so that any references can be garbage collected. If you do not need this, use 17 | // slices.Delete instead. 18 | // 19 | // Deprecated: As of Go 1.22, slices.Delete now zeroes out removed elements, so use it instead. This function was 20 | // deprecated on March 29, 2024 and will be removed on or after January 1, 2025. 21 | func ZeroedDelete[S ~[]E, E any](s S, i, j int) S { 22 | return slices.Delete(s, i, j) 23 | } 24 | 25 | // ZeroedDeleteFunc removes any elements from s for which del returns true, returning the modified slice. This function 26 | // modifies the contents of the slice s; it does not create a new slice. The elements that are removed are zeroed so 27 | // that any references can be garbage collected. If you do not need this, use slices.DeleteFunc instead. 28 | // 29 | // Deprecated: As of Go 1.22, slices.DeleteFunc now zeroes out removed elements, so use it instead. This function was 30 | // deprecated on March 29, 2024 and will be removed on or after January 1, 2025. 31 | func ZeroedDeleteFunc[S ~[]E, E any](s S, del func(E) bool) S { 32 | return slices.DeleteFunc(s, del) 33 | } 34 | -------------------------------------------------------------------------------- /collection/slice/slice_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | package slice_test 11 | 12 | import ( 13 | "testing" 14 | 15 | "github.com/richardwilkes/toolbox/check" 16 | "github.com/richardwilkes/toolbox/collection/slice" 17 | ) 18 | 19 | func TestZeroedDelete(t *testing.T) { 20 | type data struct { 21 | a int 22 | } 23 | d1 := &data{a: 1} 24 | d2 := &data{a: 2} 25 | d3 := &data{a: 3} 26 | for _, test := range []struct { 27 | s []*data 28 | want []*data 29 | i, j int 30 | }{ 31 | { 32 | []*data{d1, d2, d3}, 33 | []*data{d1, d2, d3}, 34 | 0, 35 | 0, 36 | }, 37 | { 38 | []*data{d1, d2, d3}, 39 | []*data{d2, d3}, 40 | 0, 41 | 1, 42 | }, 43 | { 44 | []*data{d1, d2, d3}, 45 | []*data{d1, d2, d3}, 46 | 3, 47 | 3, 48 | }, 49 | { 50 | []*data{d1, d2, d3}, 51 | []*data{d3}, 52 | 0, 53 | 2, 54 | }, 55 | { 56 | []*data{d1, d2, d3}, 57 | []*data{}, 58 | 0, 59 | 3, 60 | }, 61 | } { 62 | theCopy := append([]*data{}, test.s...) 63 | result := slice.ZeroedDelete(theCopy, test.i, test.j) 64 | check.Equal(t, result, test.want, "ZeroedDelete(%v, %d, %d) = %v, want %v", test.s, test.i, test.j, result, test.want) 65 | for i := len(result); i < len(theCopy); i++ { 66 | check.Nil(t, theCopy[i], "residual element %d should have been nil, was %v", i, theCopy[i]) 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /constants.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | // Package toolbox is the top level package for a collection of utility packages. 11 | package toolbox 12 | 13 | // Constants for comparison to runtime.GOOS 14 | const ( 15 | MacOS = "darwin" 16 | WindowsOS = "windows" 17 | LinuxOS = "linux" 18 | ) 19 | -------------------------------------------------------------------------------- /desktop/browser.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | // Package desktop provides desktop integration utilities. 11 | package desktop 12 | 13 | import ( 14 | "os/exec" 15 | 16 | "github.com/richardwilkes/toolbox/errs" 17 | ) 18 | 19 | // Open asks the system to open the provided path or URL. 20 | func Open(pathOrURL string) error { 21 | cmd, args := cmdAndArgs(pathOrURL) 22 | if err := exec.Command(cmd, args...).Start(); err != nil { 23 | return errs.NewWithCause("Unable to open "+pathOrURL, err) 24 | } 25 | return nil 26 | } 27 | -------------------------------------------------------------------------------- /desktop/browser_darwin.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | package desktop 11 | 12 | func cmdAndArgs(pathOrURL string) (cmdName string, args []string) { 13 | return "open", []string{pathOrURL} 14 | } 15 | -------------------------------------------------------------------------------- /desktop/browser_linux.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | // Package desktop provides desktop integration utilities. 11 | package desktop 12 | 13 | func cmdAndArgs(pathOrURL string) (cmdName string, args []string) { 14 | return "xdg-open", []string{pathOrURL} 15 | } 16 | -------------------------------------------------------------------------------- /desktop/browser_windows.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | // Package desktop provides desktop integration utilities. 11 | package desktop 12 | 13 | import "strings" 14 | 15 | func cmdAndArgs(pathOrURL string) (cmdName string, args []string) { 16 | if strings.HasPrefix(pathOrURL, "http://") || strings.HasPrefix(pathOrURL, "https://") { 17 | return "cmd", []string{"/c", "start", pathOrURL} 18 | } 19 | return "explorer", []string{pathOrURL} 20 | } 21 | -------------------------------------------------------------------------------- /errs/recovery.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | package errs 11 | 12 | // RecoveryHandler defines the callback used when panics occur with Recovery. 13 | type RecoveryHandler func(error) 14 | 15 | // Recovery provides an easy way to run code that may panic. 'handler' will be called with the panic turned into an 16 | // error. Pass in nil to silently ignore any panic. 17 | // 18 | // Typical usage: 19 | // 20 | // func runSomeCode(handler errs.RecoveryHandler) { 21 | // defer errs.Recovery(handler) 22 | // // ... run the code here ... 23 | // } 24 | func Recovery(handler RecoveryHandler) { 25 | if recovered := recover(); recovered != nil && handler != nil { 26 | err, ok := recovered.(error) 27 | if !ok { 28 | err = Newf("%+v", recovered) 29 | } 30 | defer Recovery(nil) // Guard against a bad handler implementation 31 | handler(NewWithCause("recovered from panic", err)) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /eval/example_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | package eval_test 11 | 12 | import ( 13 | "fmt" 14 | 15 | "github.com/richardwilkes/toolbox/eval" 16 | "github.com/richardwilkes/toolbox/xmath/fixed" 17 | ) 18 | 19 | func Example() { 20 | e := eval.NewFixedEvaluator[fixed.D4](nil, true) 21 | result, err := e.Evaluate("1 + sqrt(2)") 22 | if err != nil { 23 | panic(err) 24 | } 25 | fmt.Println(result) 26 | // Output: 27 | // 2.4142 28 | } 29 | -------------------------------------------------------------------------------- /eval/fixed_evaluator.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | package eval 11 | 12 | import "github.com/richardwilkes/toolbox/xmath/fixed" 13 | 14 | // NewFixedEvaluator creates a new evaluator whose number type is one of the fixed types. 15 | func NewFixedEvaluator[T fixed.Dx](resolver VariableResolver, divideByZeroReturnsZero bool) *Evaluator { 16 | return &Evaluator{ 17 | Resolver: resolver, 18 | Operators: FixedOperators[T](divideByZeroReturnsZero), 19 | Functions: FixedFunctions[T](), 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /eval/float_evaluator.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | package eval 11 | 12 | import ( 13 | "golang.org/x/exp/constraints" 14 | ) 15 | 16 | // NewFloatEvaluator creates a new evaluator whose number type is one of the constraints.Float types. 17 | func NewFloatEvaluator[T constraints.Float](resolver VariableResolver, divideByZeroReturnsZero bool) *Evaluator { 18 | return &Evaluator{ 19 | Resolver: resolver, 20 | Operators: FloatOperators[T](divideByZeroReturnsZero), 21 | Functions: FloatFunctions[T](), 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /fatal/fatal.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | package fatal 11 | 12 | import ( 13 | "github.com/richardwilkes/toolbox" 14 | "github.com/richardwilkes/toolbox/atexit" 15 | "github.com/richardwilkes/toolbox/errs" 16 | ) 17 | 18 | // IfErr checks the error and if it isn't nil, calls fatal.WithErr(err). 19 | func IfErr(err error) { 20 | if !toolbox.IsNil(err) { 21 | WithErr(err) 22 | } 23 | } 24 | 25 | // WithErr logs the error and then exits with code 1. 26 | func WithErr(err error) { 27 | errs.Log(err) 28 | atexit.Exit(1) 29 | } 30 | -------------------------------------------------------------------------------- /formats/icon/ico/encoder.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | package ico 11 | 12 | import ( 13 | "bytes" 14 | "encoding/binary" 15 | "image" 16 | "image/draw" 17 | "image/png" 18 | "io" 19 | "sort" 20 | 21 | "github.com/richardwilkes/toolbox/errs" 22 | ) 23 | 24 | // See https://en.wikipedia.org/wiki/ICO_(file_format) for information on the .ico file format. 25 | 26 | type header struct { 27 | Reserved uint16 28 | ImageType uint16 29 | Count uint16 30 | } 31 | 32 | type entry struct { 33 | Width uint8 34 | Height uint8 35 | Colors uint8 36 | Reserved uint8 37 | Planes uint16 38 | BitPerPixel uint16 39 | Size uint32 40 | Offset uint32 41 | } 42 | 43 | // Encode one or more images into an .ico. At least one image must be provided and no image may have a width or height 44 | // greater than 256 pixels. Windows recommends providing 256x256, 48x48, 32x32, and 16x16 icons. 45 | func Encode(w io.Writer, images ...image.Image) error { 46 | if len(images) == 0 { 47 | return errs.New("must supply at least 1 image") 48 | } 49 | sort.Slice(images, func(i, j int) bool { 50 | return images[i].Bounds().Dx() > images[j].Bounds().Dx() 51 | }) 52 | list := make([][]byte, 0, len(images)) 53 | for _, img := range images { 54 | bounds := img.Bounds() 55 | if bounds.Dx() > 256 || bounds.Dy() > 256 { 56 | return errs.New("image too large - .ico has a 256x256 size limit") 57 | } 58 | if _, ok := img.(*image.RGBA); !ok { 59 | m := image.NewRGBA(bounds) 60 | draw.Draw(m, bounds, img, bounds.Min, draw.Src) 61 | img = m 62 | } 63 | var buffer bytes.Buffer 64 | if err := png.Encode(&buffer, img); err != nil { 65 | return errs.Wrap(err) 66 | } 67 | list = append(list, buffer.Bytes()) 68 | } 69 | if err := binary.Write(w, binary.LittleEndian, header{ 70 | ImageType: 1, 71 | Count: uint16(len(images)), 72 | }); err != nil { 73 | return errs.Wrap(err) 74 | } 75 | offset := 6 + uint32(len(images))*16 76 | for i, img := range images { 77 | bounds := img.Bounds() 78 | width := bounds.Dx() 79 | if width > 255 { 80 | width = 0 81 | } 82 | height := bounds.Dy() 83 | if height > 255 { 84 | height = 0 85 | } 86 | e := entry{ 87 | Width: uint8(width), 88 | Height: uint8(height), 89 | Planes: 1, 90 | BitPerPixel: 32, 91 | Size: uint32(len(list[i])), 92 | Offset: offset, 93 | } 94 | if err := binary.Write(w, binary.LittleEndian, e); err != nil { 95 | return errs.Wrap(err) 96 | } 97 | offset += e.Size 98 | } 99 | for _, data := range list { 100 | if _, err := w.Write(data); err != nil { 101 | return errs.Wrap(err) 102 | } 103 | } 104 | return nil 105 | } 106 | -------------------------------------------------------------------------------- /formats/json/http.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | package json 11 | 12 | import ( 13 | "bytes" 14 | "context" 15 | "net/http" 16 | 17 | "github.com/richardwilkes/toolbox/errs" 18 | "github.com/richardwilkes/toolbox/xio" 19 | ) 20 | 21 | // GetRequest calls http.Get with the URL and returns the response body as a new Data object. 22 | func GetRequest(url string) (statusCode int, body *Data, err error) { 23 | return GetRequestWithContext(context.Background(), url) 24 | } 25 | 26 | // GetRequestWithContext calls http.Get with the URL and returns the response body as a new Data object. 27 | func GetRequestWithContext(ctx context.Context, url string) (statusCode int, body *Data, err error) { 28 | var req *http.Request 29 | req, err = http.NewRequestWithContext(ctx, http.MethodGet, url, http.NoBody) 30 | if err != nil { 31 | return 0, nil, errs.NewWithCause("unable to create request", err) 32 | } 33 | return makeRequest(req) 34 | } 35 | 36 | // PostRequest calls http.Post with the URL and the contents of this Data object and returns the response body as a new 37 | // Data object. 38 | func (j *Data) PostRequest(url string) (statusCode int, body *Data, err error) { 39 | return j.PostRequestWithContext(context.Background(), url) 40 | } 41 | 42 | // PostRequestWithContext calls http.Post with the URL and the contents of this Data object and returns the response 43 | // body as a new Data object. 44 | func (j *Data) PostRequestWithContext(ctx context.Context, url string) (statusCode int, body *Data, err error) { 45 | var req *http.Request 46 | req, err = http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewBuffer(j.Bytes())) 47 | if err != nil { 48 | return 0, nil, errs.NewWithCause("unable to create request", err) 49 | } 50 | req.Header.Set("Content-Type", "application/json") 51 | return makeRequest(req) 52 | } 53 | 54 | func makeRequest(req *http.Request) (statusCode int, body *Data, err error) { 55 | var rsp *http.Response 56 | if rsp, err = http.DefaultClient.Do(req); err != nil { 57 | return 0, &Data{}, errs.NewWithCause("request failed", err) 58 | } 59 | defer xio.DiscardAndCloseIgnoringErrors(rsp.Body) 60 | statusCode = rsp.StatusCode 61 | body = MustParseStream(rsp.Body) 62 | return 63 | } 64 | -------------------------------------------------------------------------------- /formats/xlsx/cell.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | package xlsx 11 | 12 | import ( 13 | "strconv" 14 | "time" 15 | ) 16 | 17 | // Cell types. 18 | const ( 19 | String CellType = iota 20 | Number 21 | Boolean 22 | ) 23 | 24 | // CellType holds an enumeration of cell types. 25 | type CellType int 26 | 27 | // Cell holds the contents of a cell. 28 | type Cell struct { 29 | Value string 30 | Type CellType 31 | } 32 | 33 | func (c *Cell) String() string { 34 | return c.Value 35 | } 36 | 37 | // Integer returns the value of this cell as an integer. 38 | func (c *Cell) Integer() int { 39 | v, err := strconv.Atoi(c.Value) 40 | if err != nil { 41 | v = int(c.Float()) 42 | } 43 | return v 44 | } 45 | 46 | // Float returns the value of this cell as an float. 47 | func (c *Cell) Float() float64 { 48 | f, err := strconv.ParseFloat(c.Value, 64) 49 | if err != nil { 50 | return 0 51 | } 52 | return f 53 | } 54 | 55 | // Boolean returns the value of this cell as a boolean. 56 | func (c *Cell) Boolean() bool { 57 | return c.Value != "0" 58 | } 59 | 60 | // Time returns the value of this cell as a time.Time. 61 | func (c *Cell) Time() time.Time { 62 | return timeFromExcelTime(c.Float()) 63 | } 64 | -------------------------------------------------------------------------------- /formats/xlsx/datetime.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | package xlsx 11 | 12 | import ( 13 | "math" 14 | "time" 15 | ) 16 | 17 | // TimeFromExcelTime convertx an Excel time representation to a time.Time. Code for date/time conversion adapted from 18 | // github.com/tealeg/xlsx. 19 | func timeFromExcelTime(excelTime float64) time.Time { 20 | var date time.Time 21 | intPart := int64(excelTime) 22 | // Excel uses Julian dates prior to March 1st 1900, and 23 | // Gregorian thereafter. 24 | if intPart <= 61 { 25 | return julianDateToGregorianTime(2400000.5, excelTime+15018.0) 26 | } 27 | floatPart := excelTime - float64(intPart) 28 | var dayNanoSeconds float64 = 24 * 60 * 60 * 1000 * 1000 * 1000 29 | date = time.Date(1899, 12, 30, 0, 0, 0, 0, time.UTC) 30 | durationDays := time.Duration(intPart) * time.Hour * 24 31 | durationPart := time.Duration(dayNanoSeconds * floatPart) 32 | return date.Add(durationDays).Add(durationPart) 33 | } 34 | 35 | func julianDateToGregorianTime(part1, part2 float64) time.Time { 36 | part1I, part1F := math.Modf(part1) 37 | part2I, part2F := math.Modf(part2) 38 | julianDays := part1I + part2I 39 | julianFraction := part1F + part2F 40 | julianDays, julianFraction = shiftJulianToNoon(julianDays, julianFraction) 41 | day, month, year := fliegelAndVanFlandernAlgorithm(int(julianDays)) 42 | hours, minutes, seconds, nanoseconds := fractionOfADay(julianFraction) 43 | return time.Date(year, time.Month(month), day, hours, minutes, seconds, nanoseconds, time.UTC) 44 | } 45 | 46 | func shiftJulianToNoon(julianDays, julianFraction float64) (julianDaysResult, julianFractionResult float64) { 47 | switch { 48 | case -0.5 < julianFraction && julianFraction < 0.5: 49 | julianFraction += 0.5 50 | case julianFraction >= 0.5: 51 | julianDays++ 52 | julianFraction -= 0.5 53 | case julianFraction <= -0.5: 54 | julianDays-- 55 | julianFraction += 1.5 56 | } 57 | return julianDays, julianFraction 58 | } 59 | 60 | func fliegelAndVanFlandernAlgorithm(jd int) (day, month, year int) { 61 | l := jd + 68569 62 | n := (4 * l) / 146097 63 | l -= (146097*n + 3) / 4 64 | i := (4000 * (l + 1)) / 1461001 65 | l = l - (1461*i)/4 + 31 66 | j := (80 * l) / 2447 67 | d := l - (2447*j)/80 68 | l = j / 11 69 | m := j + 2 - (12 * l) 70 | y := 100*(n-49) + i + l 71 | return d, m, y 72 | } 73 | 74 | func fractionOfADay(fraction float64) (hours, minutes, seconds, nanoseconds int) { 75 | const ( 76 | c1us = 1e3 77 | c1s = 1e9 78 | ) 79 | frac := int64(24*60*60*c1s*fraction + c1us/2) 80 | nanoseconds = int((frac%c1s)/c1us) * c1us 81 | frac /= c1s 82 | seconds = int(frac % 60) 83 | frac /= 60 84 | minutes = int(frac % 60) 85 | hours = int(frac / 60) 86 | return 87 | } 88 | -------------------------------------------------------------------------------- /formats/xlsx/ref.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | package xlsx 11 | 12 | import ( 13 | "strconv" 14 | "strings" 15 | ) 16 | 17 | const letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" 18 | 19 | // Ref holds a cell reference. 20 | type Ref struct { 21 | Row int 22 | Col int 23 | } 24 | 25 | // ParseRef parses a string into a Ref. 26 | func ParseRef(str string) Ref { 27 | r := Ref{} 28 | state := 0 29 | for _, ch := range strings.ToUpper(str) { 30 | if state == 0 { 31 | if ch >= 'A' && ch <= 'Z' { 32 | r.Col *= 26 33 | r.Col += int(1 + ch - 'A') 34 | } else { 35 | state = 1 36 | } 37 | } 38 | if state == 1 { 39 | if ch >= '0' && ch <= '9' { 40 | r.Row *= 10 41 | r.Row += int(ch - '0') 42 | } else { 43 | break 44 | } 45 | } 46 | } 47 | if r.Col > 0 { 48 | r.Col-- 49 | } 50 | if r.Row > 0 { 51 | r.Row-- 52 | } 53 | return r 54 | } 55 | 56 | func (r Ref) String() string { 57 | var a [65]byte 58 | i := len(a) 59 | col := r.Col 60 | for col >= 26 { 61 | i-- 62 | q := col / 26 63 | a[i] = letters[col-q*26] 64 | col = q - 1 65 | } 66 | i-- 67 | a[i] = letters[col] 68 | return string(a[i:]) + strconv.Itoa(r.Row+1) 69 | } 70 | -------------------------------------------------------------------------------- /formats/xlsx/ref_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | package xlsx_test 11 | 12 | import ( 13 | "testing" 14 | 15 | "github.com/richardwilkes/toolbox/check" 16 | "github.com/richardwilkes/toolbox/formats/xlsx" 17 | ) 18 | 19 | func TestRef(t *testing.T) { 20 | for i, d := range []struct { 21 | Text string 22 | Col int 23 | Row int 24 | }{ 25 | {"A1", 0, 0}, 26 | {"Z9", 25, 8}, 27 | {"AA1", 26, 0}, 28 | {"AA99", 26, 98}, 29 | {"ZZ100", 701, 99}, 30 | } { 31 | ref := xlsx.ParseRef(d.Text) 32 | check.Equal(t, d.Col, ref.Col, "column for index %d: %s", i, d.Text) 33 | check.Equal(t, d.Row, ref.Row, "row for index %d: %s", i, d.Text) 34 | check.Equal(t, d.Text, ref.String(), "String() for index %d: %s", i, d.Text) 35 | } 36 | 37 | for r := range 100 { 38 | for c := range 10000 { 39 | in := xlsx.Ref{Row: r, Col: c} 40 | out := xlsx.ParseRef(in.String()) 41 | check.Equal(t, in, out) 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/richardwilkes/toolbox 2 | 3 | go 1.24.2 4 | 5 | require ( 6 | github.com/jackpal/gateway v1.1.1 7 | github.com/pkg/term v1.1.0 8 | golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 9 | golang.org/x/image v0.27.0 10 | gopkg.in/yaml.v3 v3.0.1 11 | ) 12 | 13 | require ( 14 | github.com/davecgh/go-spew v1.1.1 // indirect 15 | github.com/pmezard/go-difflib v1.0.0 // indirect 16 | github.com/stretchr/objx v0.5.2 // indirect 17 | github.com/stretchr/testify v1.10.0 // indirect 18 | golang.org/x/net v0.40.0 // indirect 19 | golang.org/x/sys v0.33.0 // indirect 20 | ) 21 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/jackpal/gateway v1.1.1 h1:UXXXkJGIHFsStms9ZBgGpoaFEJP7oJtFn5vplIT68E8= 4 | github.com/jackpal/gateway v1.1.1/go.mod h1:Tl1vZVtUaXx5j6P5HFmv45alhEi4yHHLfT4PRbB7eyw= 5 | github.com/pkg/term v1.1.0 h1:xIAAdCMh3QIAy+5FrE8Ad8XoDhEU4ufwbaSozViP9kk= 6 | github.com/pkg/term v1.1.0/go.mod h1:E25nymQcrSllhX42Ok8MRm1+hyBdHY0dCeiKZ9jpNGw= 7 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 8 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 9 | github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= 10 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 11 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 12 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 13 | golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 h1:y5zboxd6LQAqYIhHnB48p0ByQ/GnQx2BE33L8BOHQkI= 14 | golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ= 15 | golang.org/x/image v0.27.0 h1:C8gA4oWU/tKkdCfYT6T2u4faJu3MeNS5O8UPWlPF61w= 16 | golang.org/x/image v0.27.0/go.mod h1:xbdrClrAUway1MUTEZDq9mz/UpRwYAkFFNUslZtcB+g= 17 | golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= 18 | golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= 19 | golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 20 | golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= 21 | golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 22 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 23 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 24 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 25 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 26 | -------------------------------------------------------------------------------- /i18n/localization_other.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | //go:build !windows 11 | 12 | package i18n 13 | 14 | import "os" 15 | 16 | // Locale returns the value of the LC_ALL environment variable, if set. If not, then it falls back to the value of the 17 | // LANG environment variable. If that is also not set, then it returns "en_US.UTF-8". 18 | func Locale() string { 19 | locale := os.Getenv("LC_ALL") 20 | if locale == "" { 21 | locale = os.Getenv("LANG") 22 | if locale == "" { 23 | locale = "en_US.UTF-8" 24 | } 25 | } 26 | return locale 27 | } 28 | -------------------------------------------------------------------------------- /i18n/localization_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | package i18n 11 | 12 | import ( 13 | "testing" 14 | 15 | "github.com/richardwilkes/toolbox/check" 16 | ) 17 | 18 | func TestLocalization(t *testing.T) { 19 | de := make(map[string]string) 20 | de["a"] = "1" 21 | langMap["de"] = de 22 | deDE := make(map[string]string) 23 | deDE["a"] = "2" 24 | langMap["de_dn"] = deDE 25 | Language = "de_dn.UTF-8" 26 | check.Equal(t, "2", Text("a")) 27 | Language = "de_dn" 28 | check.Equal(t, "2", Text("a")) 29 | Language = "de" 30 | check.Equal(t, "1", Text("a")) 31 | Language = "xx" 32 | check.Equal(t, "a", Text("a")) 33 | delete(langMap, "de_dn") 34 | Language = "de" 35 | check.Equal(t, "1", Text("a")) 36 | } 37 | 38 | func TestAltLocalization(t *testing.T) { 39 | check.Equal(t, "Hello!", Text("Hello!")) 40 | SetLocalizer(func(_ string) string { return "Bonjour!" }) 41 | check.Equal(t, "Bonjour!", Text("Hello!")) 42 | SetLocalizer(nil) 43 | check.Equal(t, "Hello!", Text("Hello!")) 44 | } 45 | -------------------------------------------------------------------------------- /i18n/localization_windows.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | package i18n 11 | 12 | import ( 13 | "syscall" 14 | "unsafe" 15 | ) 16 | 17 | // Locale returns the locale set for the user. If that has not been set, then it falls back to the locale set for the 18 | // system. If that is also unset, then it return "en_US.UTF-8". 19 | func Locale() string { 20 | kernel32 := syscall.NewLazyDLL("kernel32.dll") 21 | proc := kernel32.NewProc("GetUserDefaultLocaleName") 22 | buffer := make([]uint16, 128) 23 | if ret, _, _ := proc.Call(uintptr(unsafe.Pointer(&buffer[0])), uintptr(len(buffer))); ret == 0 { //nolint:errcheck // ret is the error code 24 | proc = kernel32.NewProc("GetSystemDefaultLocaleName") 25 | if ret, _, _ = proc.Call(uintptr(unsafe.Pointer(&buffer[0])), uintptr(len(buffer))); ret == 0 { //nolint:errcheck // ret is the error code 26 | return "en_US.UTF-8" 27 | } 28 | } 29 | return syscall.UTF16ToString(buffer) 30 | } 31 | -------------------------------------------------------------------------------- /log/multilog/multilog.go: -------------------------------------------------------------------------------- 1 | package multilog 2 | 3 | import ( 4 | "context" 5 | "log/slog" 6 | 7 | "github.com/richardwilkes/toolbox/errs" 8 | ) 9 | 10 | var _ slog.Handler = &Handler{} 11 | 12 | // Handler is a slog.Handler that fans out log records to multiple other handlers. 13 | type Handler struct { 14 | handlers []slog.Handler 15 | } 16 | 17 | // New creates a new Handler that fans out log records to the provided handlers. 18 | func New(handlers ...slog.Handler) *Handler { 19 | return &Handler{handlers: handlers} 20 | } 21 | 22 | // Enabled implements slog.Handler. 23 | func (h *Handler) Enabled(ctx context.Context, level slog.Level) bool { 24 | for _, h := range h.handlers { 25 | if h.Enabled(ctx, level) { 26 | return true 27 | } 28 | } 29 | return false 30 | } 31 | 32 | // WithGroup implements slog.Handler. 33 | func (h *Handler) WithGroup(name string) slog.Handler { 34 | if name == "" { 35 | return h 36 | } 37 | handlers := make([]slog.Handler, len(h.handlers)) 38 | for i, one := range h.handlers { 39 | handlers[i] = one.WithGroup(name) 40 | } 41 | return &Handler{handlers: handlers} 42 | } 43 | 44 | // WithAttrs implements slog.Handler. 45 | func (h *Handler) WithAttrs(attrs []slog.Attr) slog.Handler { 46 | handlers := make([]slog.Handler, len(h.handlers)) 47 | for i, one := range h.handlers { 48 | handlers[i] = one.WithAttrs(attrs) 49 | } 50 | return &Handler{handlers: handlers} 51 | } 52 | 53 | // Handle implements slog.Handler. 54 | func (h *Handler) Handle(ctx context.Context, r slog.Record) error { //nolint:gocritic // Must use defined API 55 | var result error 56 | for _, one := range h.handlers { 57 | if one.Enabled(ctx, r.Level) { 58 | result = errs.Append(result, runHandler(ctx, &r, one)) 59 | } 60 | } 61 | return result 62 | } 63 | 64 | func runHandler(ctx context.Context, r *slog.Record, h slog.Handler) (err error) { 65 | defer errs.Recovery(func(rerr error) { err = rerr }) 66 | err = h.Handle(ctx, r.Clone()) 67 | return err 68 | } 69 | -------------------------------------------------------------------------------- /log/rotation/cmdline.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | package rotation 11 | 12 | import ( 13 | "io" 14 | "log" 15 | "os" 16 | 17 | "github.com/richardwilkes/toolbox/cmdline" 18 | "github.com/richardwilkes/toolbox/errs" 19 | "github.com/richardwilkes/toolbox/i18n" 20 | "github.com/richardwilkes/toolbox/xio" 21 | ) 22 | 23 | // PathToLog holds the path to the log file that was configured on the command line when using ParseAndSetup(). 24 | var PathToLog string 25 | 26 | // ParseAndSetupLogging adds command-line options for controlling logging, parses the command line, then instantiates a 27 | // rotator and attaches it to slog. Returns the remaining arguments that weren't used for option content. If 28 | // consoleOnByDefault is true, then logs will also go to the console by default, but an option to turn them off will be 29 | // added to the command line flags. Conversely, if it is false, an option to turn them on will be added to the command 30 | // line flags. 31 | func ParseAndSetupLogging(cl *cmdline.CmdLine, consoleOnByDefault bool) []string { 32 | logFile := DefaultPath() 33 | var maxSize int64 = DefaultMaxSize 34 | maxBackups := DefaultMaxBackups 35 | consoleOption := false 36 | cl.NewGeneralOption(&logFile).SetSingle('l').SetName("log-file").SetUsage(i18n.Text("The file to write logs to")) 37 | cl.NewGeneralOption(&maxSize).SetName("log-file-size").SetUsage(i18n.Text("The maximum number of bytes to write to a log file before rotating it")) 38 | cl.NewGeneralOption(&maxBackups).SetName("log-file-backups").SetUsage(i18n.Text("The maximum number of old logs files to retain")) 39 | opt := cl.NewGeneralOption(&consoleOption) 40 | if consoleOnByDefault { 41 | opt.SetSingle('q').SetName("quiet").SetUsage(i18n.Text("Suppress the log output to the console")) 42 | } else { 43 | opt.SetSingle('C').SetName("log-to-console").SetUsage(i18n.Text("Copy the log output to the console")) 44 | } 45 | remainingArgs := cl.Parse(os.Args[1:]) 46 | if rotator, err := New(Path(logFile), MaxSize(maxSize), MaxBackups(maxBackups)); err == nil { 47 | if consoleOnByDefault == consoleOption { 48 | log.SetOutput(rotator) 49 | } else { 50 | log.SetOutput(&xio.TeeWriter{Writers: []io.Writer{rotator, os.Stdout}}) 51 | } 52 | PathToLog = rotator.PathToLog() 53 | } else { 54 | errs.Log(err) 55 | } 56 | return remainingArgs 57 | } 58 | -------------------------------------------------------------------------------- /log/rotation/options.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | package rotation 11 | 12 | import ( 13 | "os" 14 | "path/filepath" 15 | 16 | "github.com/richardwilkes/toolbox/cmdline" 17 | "github.com/richardwilkes/toolbox/errs" 18 | "github.com/richardwilkes/toolbox/xio/fs/paths" 19 | ) 20 | 21 | // Constants for defaults. 22 | const ( 23 | DefaultMaxSize = 10 * 1024 * 1024 24 | DefaultMaxBackups = 1 25 | ) 26 | 27 | // DefaultPath returns the default path that will be used. This will use cmdline.AppIdentifier (if set) to better 28 | // isolate the log location. 29 | func DefaultPath() string { 30 | return filepath.Join(paths.AppLogDir(), cmdline.AppCmdName+".log") 31 | } 32 | 33 | // Path specifies the file to write logs to. Backup log files will be retained in the same directory. Defaults to the 34 | // value of DefaultPath(). 35 | func Path(path string) func(*Rotator) error { 36 | return func(r *Rotator) error { 37 | if path == "" { 38 | return errs.New("Must specify a path") 39 | } 40 | r.path = path 41 | return nil 42 | } 43 | } 44 | 45 | // MaxSize sets the maximum size of the log file before it gets rotated. Defaults to DefaultMaxSize. 46 | func MaxSize(maxSize int64) func(*Rotator) error { 47 | return func(r *Rotator) error { 48 | r.maxSize = maxSize 49 | return nil 50 | } 51 | } 52 | 53 | // MaxBackups sets the maximum number of old log files to retain. Defaults to DefaultMaxBackups. 54 | func MaxBackups(maxBackups int) func(*Rotator) error { 55 | return func(r *Rotator) error { 56 | r.maxBackups = maxBackups 57 | return nil 58 | } 59 | } 60 | 61 | // WithMask sets the mask when creating files, which have the unmasked mode of 0644, and directories, which have the 62 | // unmasked mode of 0755. Defaults to 0777. 63 | func WithMask(mask os.FileMode) func(*Rotator) error { 64 | return func(r *Rotator) error { 65 | r.mask = mask 66 | return nil 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /log/rotation/rotator_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | package rotation_test 11 | 12 | import ( 13 | "fmt" 14 | "os" 15 | "path/filepath" 16 | "testing" 17 | 18 | "github.com/richardwilkes/toolbox/check" 19 | "github.com/richardwilkes/toolbox/log/rotation" 20 | ) 21 | 22 | const ( 23 | maxSize = 100 24 | maxBackups = 2 25 | ) 26 | 27 | func TestRotator(t *testing.T) { 28 | tmpdir, err := os.MkdirTemp("", "rotator_test_") 29 | check.NoError(t, err) 30 | defer cleanup(t, tmpdir) 31 | 32 | logFiles := []string{filepath.Join(tmpdir, "test.log")} 33 | for i := range maxBackups { 34 | logFiles = append(logFiles, fmt.Sprintf("%s-%d", logFiles[0], i+1)) 35 | } 36 | 37 | r, err := rotation.New(rotation.Path(logFiles[0]), rotation.MaxSize(maxSize), rotation.MaxBackups(maxBackups)) 38 | check.NoError(t, err) 39 | _, err = os.Stat(logFiles[0]) 40 | check.Error(t, err) 41 | check.True(t, os.IsNotExist(err)) 42 | for i := range maxSize * (2 + maxBackups) { 43 | _, err = fmt.Fprintln(r, i) 44 | check.NoError(t, err) 45 | } 46 | _, err = fmt.Fprintln(r, "goodbye") 47 | check.NoError(t, err) 48 | check.NoError(t, r.Close()) 49 | for _, f := range logFiles { 50 | fi, fErr := os.Stat(f) 51 | check.NoError(t, fErr) 52 | check.True(t, fi.Size() <= maxSize) 53 | } 54 | 55 | r, err = rotation.New(rotation.Path(logFiles[0]), rotation.MaxSize(maxSize), rotation.MaxBackups(maxBackups)) 56 | check.NoError(t, err) 57 | _, err = fmt.Fprintln(r, "hello") 58 | check.NoError(t, err) 59 | check.NoError(t, r.Close()) 60 | } 61 | 62 | func cleanup(t *testing.T, path string) { 63 | t.Helper() 64 | check.NoError(t, os.RemoveAll(path)) 65 | } 66 | -------------------------------------------------------------------------------- /nil.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | package toolbox 11 | 12 | import "reflect" 13 | 14 | // IsNil returns true if the interface is nil or if the value it points to is nil. 15 | func IsNil(i any) bool { 16 | if i == nil { 17 | return true 18 | } 19 | switch reflect.TypeOf(i).Kind() { 20 | case reflect.Chan, 21 | reflect.Func, 22 | reflect.Interface, 23 | reflect.Map, 24 | reflect.Pointer, 25 | reflect.Slice, 26 | reflect.UnsafePointer: 27 | return reflect.ValueOf(i).IsNil() 28 | default: 29 | return false 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /rate/interface.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | package rate 11 | 12 | // Limiter provides a rate limiter. 13 | type Limiter interface { 14 | // New returns a new limiter that is subordinate to this limiter, meaning that its cap rate is also capped by its 15 | // parent. 16 | New(capacity int) Limiter 17 | 18 | // Cap returns the capacity per time period. 19 | Cap(applyParentCaps bool) int 20 | 21 | // SetCap sets the capacity. 22 | SetCap(capacity int) 23 | 24 | // LastUsed returns the capacity used in the last time period. 25 | LastUsed() int 26 | 27 | // Use returns a channel that will return nil when the request is successful, or an error if the request cannot be 28 | // fulfilled. 29 | Use(amount int) <-chan error 30 | 31 | // Closed returns true if the limiter is closed. 32 | Closed() bool 33 | 34 | // Close this limiter and any children it may have. 35 | Close() 36 | } 37 | -------------------------------------------------------------------------------- /rate/limiter_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | package rate_test 11 | 12 | import ( 13 | "testing" 14 | "time" 15 | 16 | "github.com/richardwilkes/toolbox/check" 17 | "github.com/richardwilkes/toolbox/rate" 18 | ) 19 | 20 | func TestCap(t *testing.T) { 21 | rl := rate.New(50*1024, time.Second) 22 | check.Equal(t, 50*1024, rl.Cap(true)) 23 | sub := rl.New(100 * 1024) 24 | check.Equal(t, 100*1024, sub.Cap(false)) 25 | check.Equal(t, 50*1024, sub.Cap(true)) 26 | sub.SetCap(1024) 27 | check.Equal(t, 1024, sub.Cap(true)) 28 | rl.Close() 29 | check.True(t, sub.Closed()) 30 | check.True(t, rl.Closed()) 31 | } 32 | 33 | func TestUse(t *testing.T) { 34 | rl := rate.New(100, 100*time.Millisecond) 35 | endAfter := time.Now().Add(250 * time.Millisecond) 36 | for endAfter.After(time.Now()) { 37 | err := <-rl.Use(1) 38 | check.NoError(t, err) 39 | } 40 | check.Equal(t, 100, rl.LastUsed()) 41 | rl.Close() 42 | check.True(t, rl.Closed()) 43 | } 44 | -------------------------------------------------------------------------------- /softref/softref.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | package softref 11 | 12 | import ( 13 | "log/slog" 14 | "runtime" 15 | "sync" 16 | ) 17 | 18 | // Pool is used to track soft references to resources. 19 | type Pool struct { 20 | refs map[string]*softRef 21 | lock sync.Mutex 22 | } 23 | 24 | // Resource is a resource that will be used with a pool. 25 | type Resource interface { 26 | // Key returns a unique key for this resource. Must never change. 27 | Key() string 28 | // Release is called when the resource is no longer being referenced by any remaining soft references. 29 | Release() 30 | } 31 | 32 | // SoftRef is a soft reference to a given resource. 33 | type SoftRef struct { 34 | Resource Resource 35 | Key string 36 | } 37 | 38 | type softRef struct { 39 | resource Resource 40 | count int 41 | } 42 | 43 | // DefaultPool is a global default soft reference pool. 44 | var DefaultPool = NewPool() 45 | 46 | // NewPool creates a new soft reference pool. 47 | func NewPool() *Pool { 48 | return &Pool{refs: make(map[string]*softRef)} 49 | } 50 | 51 | // NewSoftRef returns a SoftRef to the given resource, along with a flag indicating if a reference existed previously. 52 | func (p *Pool) NewSoftRef(resource Resource) (ref *SoftRef, existedPreviously bool) { 53 | key := resource.Key() 54 | p.lock.Lock() 55 | defer p.lock.Unlock() 56 | r := p.refs[key] 57 | if r != nil { 58 | r.count++ 59 | } else { 60 | r = &softRef{ 61 | resource: resource, 62 | count: 1, 63 | } 64 | p.refs[key] = r 65 | } 66 | sr := &SoftRef{ 67 | Key: key, 68 | Resource: r.resource, 69 | } 70 | runtime.SetFinalizer(sr, p.finalizeSoftRef) 71 | return sr, r.count > 1 72 | } 73 | 74 | func (p *Pool) finalizeSoftRef(ref *SoftRef) { 75 | p.lock.Lock() 76 | if r, ok := p.refs[ref.Key]; ok { 77 | r.count-- 78 | if r.count == 0 { 79 | delete(p.refs, ref.Key) 80 | r.resource.Release() 81 | } else if r.count < 0 { 82 | slog.Debug("SoftRef count is invalid", "key", ref.Key, "count", r.count) 83 | } 84 | } else { 85 | slog.Debug("SoftRef finalized for unknown key", "key", ref.Key) 86 | } 87 | p.lock.Unlock() 88 | } 89 | -------------------------------------------------------------------------------- /softref/softref_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | package softref_test 11 | 12 | import ( 13 | "runtime" 14 | "testing" 15 | "time" 16 | 17 | "github.com/richardwilkes/toolbox/check" 18 | "github.com/richardwilkes/toolbox/softref" 19 | ) 20 | 21 | type res struct { 22 | released chan<- string 23 | key string 24 | } 25 | 26 | func newRes(key string, released chan<- string) *res { 27 | return &res{ 28 | key: key, 29 | released: released, 30 | } 31 | } 32 | 33 | func (r *res) Key() string { 34 | return r.key 35 | } 36 | 37 | func (r *res) Release() { 38 | r.released <- r.key 39 | } 40 | 41 | func TestSoftRef(t *testing.T) { 42 | p := softref.NewPool() 43 | ch := make(chan string, 128) 44 | sr1, existed := p.NewSoftRef(newRes("1", ch)) 45 | check.False(t, existed) 46 | _, existed = p.NewSoftRef(newRes("2", ch)) 47 | check.False(t, existed) 48 | sr3, existed := p.NewSoftRef(newRes("3", ch)) 49 | check.False(t, existed) 50 | r4 := newRes("4", ch) 51 | sr4a, existed := p.NewSoftRef(r4) 52 | check.False(t, existed) 53 | sr4b, existed := p.NewSoftRef(r4) 54 | check.True(t, existed) 55 | lookFor(t, "2", ch) 56 | get, existed := sr3.Resource.(*res) 57 | check.True(t, existed) 58 | lookFor(t, get.key, ch) 59 | get, existed = sr1.Resource.(*res) 60 | check.True(t, existed) 61 | lookFor(t, get.key, ch) 62 | get, existed = sr4a.Resource.(*res) 63 | check.True(t, existed) 64 | get2, existed2 := sr4b.Resource.(*res) 65 | check.True(t, existed2) 66 | check.Equal(t, get.key, get2.key) 67 | lookForExpectingTimeout(t, ch) 68 | check.Equal(t, "4", sr4b.Key) // Keeps refs to r4 alive for the above call 69 | lookFor(t, get.key, ch) 70 | } 71 | 72 | func lookFor(t *testing.T, key string, ch <-chan string) { 73 | t.Helper() 74 | runtime.GC() 75 | select { 76 | case <-time.After(time.Second): 77 | t.Errorf("timed out waiting for %s", key) 78 | case k := <-ch: 79 | check.Equal(t, key, k) 80 | } 81 | } 82 | 83 | func lookForExpectingTimeout(t *testing.T, ch <-chan string) { 84 | t.Helper() 85 | runtime.GC() 86 | select { 87 | case <-time.After(time.Second): 88 | case k := <-ch: 89 | t.Errorf("received key '%s' when none expected", k) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /taskqueue/taskqueue_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | package taskqueue_test 11 | 12 | import ( 13 | "sync/atomic" 14 | "testing" 15 | 16 | "github.com/richardwilkes/toolbox/check" 17 | "github.com/richardwilkes/toolbox/taskqueue" 18 | ) 19 | 20 | const ( 21 | parallelWorkSubmissions = 10000 22 | workTotal = 49995000 23 | ) 24 | 25 | var ( 26 | prev int 27 | counter int 28 | total int32 29 | count int32 30 | ) 31 | 32 | func TestSerialQueue(t *testing.T) { 33 | q := taskqueue.New(taskqueue.Depth(100), taskqueue.Workers(1)) 34 | prev = -1 35 | counter = 0 36 | for i := range 200 { 37 | submitSerial(q, i) 38 | } 39 | q.Shutdown() 40 | check.Equal(t, 199, prev) 41 | check.Equal(t, 200, counter) 42 | } 43 | 44 | func submitSerial(q *taskqueue.Queue, i int) { 45 | q.Submit(func() { 46 | if i-1 == prev { 47 | prev = i 48 | counter++ 49 | } 50 | }) 51 | } 52 | 53 | func TestParallelQueue(t *testing.T) { 54 | q := taskqueue.New(taskqueue.Workers(5)) 55 | total = 0 56 | count = 0 57 | for i := range parallelWorkSubmissions { 58 | submitParallel(q, i) 59 | } 60 | q.Shutdown() 61 | check.Equal(t, parallelWorkSubmissions, int(count)) 62 | check.Equal(t, workTotal, int(total)) 63 | } 64 | 65 | func submitParallel(q *taskqueue.Queue, i int) { 66 | q.Submit(func() { 67 | atomic.AddInt32(&total, int32(i)) 68 | atomic.AddInt32(&count, 1) 69 | }) 70 | } 71 | 72 | func TestRecovery(t *testing.T) { 73 | check.Panics(t, boom) 74 | logged := false 75 | check.NotPanics(t, func() { 76 | q := taskqueue.New(taskqueue.RecoveryHandler(func(_ error) { logged = true })) 77 | q.Submit(boom) 78 | q.Shutdown() 79 | }) 80 | check.True(t, logged) 81 | } 82 | 83 | func TestRecoveryWithBadLogger(t *testing.T) { 84 | check.Panics(t, boom) 85 | check.NotPanics(t, func() { 86 | q := taskqueue.New(taskqueue.RecoveryHandler(func(_ error) { boom() })) 87 | q.Submit(boom) 88 | q.Shutdown() 89 | }) 90 | } 91 | 92 | func boom() { 93 | var bad *int 94 | *bad = 1 //nolint:govet // Yes, this is an intentional store to a nil pointer 95 | } 96 | -------------------------------------------------------------------------------- /tid/tid.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | package tid 11 | 12 | import ( 13 | "crypto/rand" 14 | "encoding/base64" 15 | "fmt" 16 | "strings" 17 | 18 | "github.com/richardwilkes/toolbox/errs" 19 | ) 20 | 21 | // TID is a unique identifier. These are similar to v4 UUIDs, but are shorter and have a different format that includes 22 | // a kind byte as the first character. TIDs are 17 characters long, are URL safe, and contain 96 bits of entropy. 23 | type TID string 24 | 25 | // KindAlphabet is the set of characters that can be used as the first character of a TID. The kind has no intrinsic 26 | // meaning, but can be used to differentiate between different types of ids. 27 | const KindAlphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" 28 | 29 | // MustNewTID creates a new TID with a random value and the specified kind. If an error occurs, this function panics. 30 | func MustNewTID(kind byte) TID { 31 | tid, err := NewTID(kind) 32 | if err != nil { 33 | panic(err) 34 | } 35 | return tid 36 | } 37 | 38 | // NewTID creates a new TID with a random value and the specified kind. 39 | func NewTID(kind byte) (TID, error) { 40 | if strings.IndexByte(KindAlphabet, kind) == -1 { 41 | return "", errs.New("invalid kind") 42 | } 43 | var buffer [12]byte 44 | if _, err := rand.Read(buffer[:]); err != nil { 45 | return "", errs.Wrap(err) 46 | } 47 | return TID(fmt.Sprintf("%c%s", kind, base64.RawURLEncoding.EncodeToString(buffer[:]))), nil 48 | } 49 | 50 | // FromString converts a string to a TID. 51 | func FromString(id string) (TID, error) { 52 | tid := TID(id) 53 | if !IsValid(tid) { 54 | return "", errs.New("invalid TID") 55 | } 56 | return tid, nil 57 | } 58 | 59 | // FromStringOfKind converts a string to a TID and verifies that it has the specified kind. 60 | func FromStringOfKind(id string, kind byte) (TID, error) { 61 | tid := TID(id) 62 | if !IsKindAndValid(tid, kind) { 63 | return "", errs.New("invalid TID") 64 | } 65 | return tid, nil 66 | } 67 | 68 | // IsValid returns true if the TID is a valid TID. 69 | func IsValid(id TID) bool { 70 | if len(id) != 17 || strings.IndexByte(KindAlphabet, id[0]) == -1 { 71 | return false 72 | } 73 | _, err := base64.RawURLEncoding.DecodeString(string(id[1:])) 74 | return err == nil 75 | } 76 | 77 | // IsKind returns true if the TID has the specified kind. 78 | func IsKind(id TID, kind byte) bool { 79 | return len(id) == 17 && id[0] == kind && strings.IndexByte(KindAlphabet, kind) != -1 80 | } 81 | 82 | // IsKindAndValid returns true if the TID is a valid TID with the specified kind. 83 | func IsKindAndValid(id TID, kind byte) bool { 84 | if !IsKind(id, kind) { 85 | return false 86 | } 87 | _, err := base64.RawURLEncoding.DecodeString(string(id[1:])) 88 | return err == nil 89 | } 90 | -------------------------------------------------------------------------------- /txt/all_caps.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | // Package txt provides various text utilities. 11 | package txt 12 | 13 | import ( 14 | _ "embed" 15 | "fmt" 16 | "regexp" 17 | "strings" 18 | 19 | "github.com/richardwilkes/toolbox/errs" 20 | "github.com/richardwilkes/toolbox/fatal" 21 | ) 22 | 23 | //go:embed all_caps.txt 24 | var stdAllCaps string 25 | 26 | // StdAllCaps provides the standard list of words that golint expects to be capitalized, found in the variable 27 | // 'commonInitialisms' in https://github.com/golang/lint/blob/master/lint.go#L771-L808 28 | var StdAllCaps = MustNewAllCaps(strings.Split(NormalizeLineEndings(stdAllCaps), "\n")...) 29 | 30 | // AllCaps holds information for transforming text with ToCamelCaseWithExceptions. 31 | type AllCaps struct { 32 | regex *regexp.Regexp 33 | } 34 | 35 | // NewAllCaps takes a list of words that should be all uppercase when part of a camel-cased string. 36 | func NewAllCaps(in ...string) (*AllCaps, error) { 37 | var buffer strings.Builder 38 | for _, str := range in { 39 | if buffer.Len() > 0 { 40 | buffer.WriteByte('|') 41 | } 42 | buffer.WriteString(FirstToUpper(strings.ToLower(str))) 43 | } 44 | r, err := regexp.Compile(fmt.Sprintf("(%s)(?:$|[A-Z])", buffer.String())) 45 | if err != nil { 46 | return nil, errs.Wrap(err) 47 | } 48 | return &AllCaps{regex: r}, nil 49 | } 50 | 51 | // MustNewAllCaps takes a list of words that should be all uppercase when part of a camel-cased string. Failure to 52 | // create the AllCaps object causes the program to exit. 53 | func MustNewAllCaps(in ...string) *AllCaps { 54 | result, err := NewAllCaps(in...) 55 | fatal.IfErr(err) 56 | return result 57 | } 58 | -------------------------------------------------------------------------------- /txt/all_caps.txt: -------------------------------------------------------------------------------- 1 | acl 2 | api 3 | ascii 4 | cpu 5 | css 6 | dns 7 | eof 8 | guid 9 | html 10 | http 11 | https 12 | id 13 | ip 14 | json 15 | lhs 16 | qps 17 | ram 18 | rhs 19 | rpc 20 | sla 21 | smtp 22 | sql 23 | ssh 24 | tcp 25 | tls 26 | ttl 27 | udp 28 | ui 29 | uid 30 | uuid 31 | uri 32 | url 33 | utf8 34 | vm 35 | xml 36 | xmpp 37 | xsrf 38 | xss -------------------------------------------------------------------------------- /txt/capitalize.go: -------------------------------------------------------------------------------- 1 | package txt 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | // CapitalizeWords capitalizes the first letter of each word in a string. 8 | func CapitalizeWords(s string) string { 9 | words := strings.Fields(s) 10 | for i, word := range words { 11 | words[i] = FirstToUpper(strings.ToLower(word)) 12 | } 13 | return strings.Join(words, " ") 14 | } 15 | -------------------------------------------------------------------------------- /txt/case.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | package txt 11 | 12 | import ( 13 | "unicode" 14 | "unicode/utf8" 15 | ) 16 | 17 | // ToCamelCase converts a string to CamelCase. 18 | func ToCamelCase(in string) string { 19 | runes := []rune(in) 20 | out := make([]rune, 0, len(runes)) 21 | up := true 22 | for _, r := range runes { 23 | if r == '_' { 24 | up = true 25 | } else { 26 | if up { 27 | r = unicode.ToUpper(r) 28 | up = false 29 | } 30 | out = append(out, r) 31 | } 32 | } 33 | return string(out) 34 | } 35 | 36 | // ToCamelCaseWithExceptions converts a string to CamelCase, but forces certain words to all caps. 37 | func ToCamelCaseWithExceptions(in string, exceptions *AllCaps) string { 38 | out := ToCamelCase(in) 39 | pos := 0 40 | runes := []rune(out) 41 | rr := RuneReader{} 42 | for { 43 | rr.Src = runes[pos:] 44 | rr.Pos = 0 45 | matches := exceptions.regex.FindReaderIndex(&rr) 46 | if len(matches) == 0 { 47 | break 48 | } 49 | for i := matches[0] + 1; i < matches[1]; i++ { 50 | runes[pos+i] = unicode.ToUpper(runes[pos+i]) 51 | } 52 | pos += matches[0] + 1 53 | } 54 | return string(runes) 55 | } 56 | 57 | // ToSnakeCase converts a string to snake_case. 58 | func ToSnakeCase(in string) string { 59 | runes := []rune(in) 60 | out := make([]rune, 0, 1+len(runes)) 61 | for i := range runes { 62 | if i > 0 && unicode.IsUpper(runes[i]) && ((i+1 < len(runes) && unicode.IsLower(runes[i+1])) || unicode.IsLower(runes[i-1])) { 63 | out = append(out, '_') 64 | } 65 | out = append(out, unicode.ToLower(runes[i])) 66 | } 67 | return string(out) 68 | } 69 | 70 | // FirstToUpper converts the first character to upper case. 71 | func FirstToUpper(in string) string { 72 | if in == "" { 73 | return in 74 | } 75 | r, size := utf8.DecodeRuneInString(in) 76 | if r == utf8.RuneError { 77 | return in 78 | } 79 | return string(unicode.ToUpper(r)) + in[size:] 80 | } 81 | 82 | // FirstToLower converts the first character to lower case. 83 | func FirstToLower(in string) string { 84 | if in == "" { 85 | return in 86 | } 87 | r, size := utf8.DecodeRuneInString(in) 88 | if r == utf8.RuneError { 89 | return in 90 | } 91 | return string(unicode.ToLower(r)) + in[size:] 92 | } 93 | -------------------------------------------------------------------------------- /txt/case_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | package txt_test 11 | 12 | import ( 13 | "testing" 14 | 15 | "github.com/richardwilkes/toolbox/check" 16 | "github.com/richardwilkes/toolbox/txt" 17 | ) 18 | 19 | func TestToCamelCase(t *testing.T) { 20 | check.Equal(t, "SnakeCase", txt.ToCamelCase("snake_case")) 21 | check.Equal(t, "SnakeCase", txt.ToCamelCase("snake__case")) 22 | check.Equal(t, "CamelCase", txt.ToCamelCase("CamelCase")) 23 | } 24 | 25 | func TestToCamelCaseWithExceptions(t *testing.T) { 26 | check.Equal(t, "ID", txt.ToCamelCaseWithExceptions("id", txt.StdAllCaps)) 27 | check.Equal(t, "世界ID", txt.ToCamelCaseWithExceptions("世界_id", txt.StdAllCaps)) 28 | check.Equal(t, "OneID", txt.ToCamelCaseWithExceptions("one_id", txt.StdAllCaps)) 29 | check.Equal(t, "IDOne", txt.ToCamelCaseWithExceptions("id_one", txt.StdAllCaps)) 30 | check.Equal(t, "OneIDTwo", txt.ToCamelCaseWithExceptions("one_id_two", txt.StdAllCaps)) 31 | check.Equal(t, "OneIDTwoID", txt.ToCamelCaseWithExceptions("one_id_two_id", txt.StdAllCaps)) 32 | check.Equal(t, "OneIDID", txt.ToCamelCaseWithExceptions("one_id_id", txt.StdAllCaps)) 33 | check.Equal(t, "Orchid", txt.ToCamelCaseWithExceptions("orchid", txt.StdAllCaps)) 34 | check.Equal(t, "OneURLTwo", txt.ToCamelCaseWithExceptions("one_url_two", txt.StdAllCaps)) 35 | check.Equal(t, "URLID", txt.ToCamelCaseWithExceptions("url_id", txt.StdAllCaps)) 36 | } 37 | 38 | func TestToSnakeCase(t *testing.T) { 39 | check.Equal(t, "snake_case", txt.ToSnakeCase("snake_case")) 40 | check.Equal(t, "camel_case", txt.ToSnakeCase("CamelCase")) 41 | } 42 | -------------------------------------------------------------------------------- /txt/collapse_spaces.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | package txt 11 | 12 | import "strings" 13 | 14 | // CollapseSpaces removes leading and trailing spaces and reduces any runs of two or more spaces to a single space. 15 | func CollapseSpaces(in string) string { 16 | var buffer strings.Builder 17 | lastWasSpace := false 18 | for i, r := range in { 19 | if r == ' ' { 20 | if !lastWasSpace { 21 | if i != 0 { 22 | buffer.WriteByte(' ') 23 | } 24 | lastWasSpace = true 25 | } 26 | } else { 27 | buffer.WriteRune(r) 28 | lastWasSpace = false 29 | } 30 | } 31 | str := buffer.String() 32 | if lastWasSpace && str != "" { 33 | str = str[:len(str)-1] 34 | } 35 | return str 36 | } 37 | -------------------------------------------------------------------------------- /txt/collapse_spaces_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | package txt_test 11 | 12 | import ( 13 | "testing" 14 | 15 | "github.com/richardwilkes/toolbox/check" 16 | "github.com/richardwilkes/toolbox/txt" 17 | ) 18 | 19 | func TestCollapseSpaces(t *testing.T) { 20 | data := []string{ 21 | "123", "123", 22 | " 123", "123", 23 | " 123 ", "123", 24 | " abc ", "abc", 25 | " a b c d", "a b c d", 26 | "", "", 27 | " ", "", 28 | } 29 | for i := 0; i < len(data); i += 2 { 30 | check.Equal(t, data[i+1], txt.CollapseSpaces(data[i])) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /txt/comma.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | package txt 11 | 12 | import ( 13 | "fmt" 14 | "strings" 15 | 16 | "golang.org/x/exp/constraints" 17 | ) 18 | 19 | // Comma returns text version of the value that uses commas for every 3 orders of magnitude. 20 | func Comma[T constraints.Integer | constraints.Float](value T) string { 21 | return CommaFromStringNum(fmt.Sprintf("%v", value)) 22 | } 23 | 24 | // CommaFromStringNum returns a revised version of the numeric input string that uses commas for every 3 orders of 25 | // magnitude. 26 | func CommaFromStringNum(s string) string { 27 | var buffer strings.Builder 28 | if strings.HasPrefix(s, "-") { 29 | buffer.WriteByte('-') 30 | s = s[1:] 31 | } 32 | parts := strings.Split(s, ".") 33 | i := 0 34 | needComma := false 35 | if len(parts[0])%3 != 0 { 36 | i += len(parts[0]) % 3 37 | buffer.WriteString(parts[0][:i]) 38 | needComma = true 39 | } 40 | for ; i < len(parts[0]); i += 3 { 41 | if needComma { 42 | buffer.WriteByte(',') 43 | } else { 44 | needComma = true 45 | } 46 | buffer.WriteString(parts[0][i : i+3]) 47 | } 48 | if len(parts) > 1 { 49 | buffer.Write([]byte{'.'}) 50 | buffer.WriteString(parts[1]) 51 | } 52 | return buffer.String() 53 | } 54 | -------------------------------------------------------------------------------- /txt/digits.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | package txt 11 | 12 | import ( 13 | "fmt" 14 | "unicode" 15 | 16 | "github.com/richardwilkes/toolbox/errs" 17 | ) 18 | 19 | // DigitToValue converts a unicode digit into a numeric value. 20 | func DigitToValue(ch rune) (int, error) { 21 | if ch < '\U00010000' { 22 | r16 := uint16(ch) 23 | for _, one := range unicode.Digit.R16 { 24 | if one.Lo <= r16 && one.Hi >= r16 { 25 | return int(r16 - one.Lo), nil 26 | } 27 | } 28 | } else { 29 | r32 := uint32(ch) 30 | for _, one := range unicode.Digit.R32 { 31 | if one.Lo <= r32 && one.Hi >= r32 { 32 | return int(r32 - one.Lo), nil 33 | } 34 | } 35 | } 36 | return 0, errs.New(fmt.Sprintf("Not a digit: '%v'", ch)) 37 | } 38 | -------------------------------------------------------------------------------- /txt/digits_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | package txt_test 11 | 12 | import ( 13 | "testing" 14 | 15 | "github.com/richardwilkes/toolbox/check" 16 | "github.com/richardwilkes/toolbox/txt" 17 | ) 18 | 19 | func TestDigitToValue(t *testing.T) { 20 | checkDigitToValue('5', 5, t) 21 | checkDigitToValue('٥', 5, t) 22 | checkDigitToValue('𑁯', 9, t) 23 | _, err := txt.DigitToValue('a') 24 | check.Error(t, err) 25 | } 26 | 27 | func checkDigitToValue(ch rune, expected int, t *testing.T) { 28 | value, err := txt.DigitToValue(ch) 29 | check.NoError(t, err) 30 | check.Equal(t, expected, value) 31 | } 32 | -------------------------------------------------------------------------------- /txt/duration_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | package txt_test 11 | 12 | import ( 13 | "fmt" 14 | "testing" 15 | "time" 16 | 17 | "github.com/richardwilkes/toolbox/check" 18 | "github.com/richardwilkes/toolbox/txt" 19 | ) 20 | 21 | func TestFormatDuration(t *testing.T) { 22 | for i, one := range []struct { 23 | Expected string 24 | Duration time.Duration 25 | IncludeMillis bool 26 | }{ 27 | {"0:00:00.001", time.Millisecond, true}, 28 | {"0:00:01.000", 1000 * time.Millisecond, true}, 29 | {"0:00:01", 1001 * time.Millisecond, false}, 30 | {"0:00:01", 1999 * time.Millisecond, false}, 31 | {"0:01:01", 61 * time.Second, false}, 32 | {"1:01:00", 61 * time.Minute, false}, 33 | {"61:00:00", 61 * time.Hour, false}, 34 | } { 35 | check.Equal(t, one.Expected, txt.FormatDuration(one.Duration, one.IncludeMillis), "Index %d", i) 36 | } 37 | } 38 | 39 | func TestParseDuration(t *testing.T) { 40 | for i, one := range []struct { 41 | Input string 42 | ExpectedDuration time.Duration 43 | ExpectErr bool 44 | }{ 45 | {"0:00:00.001", time.Millisecond, false}, 46 | {"0.001", 0, true}, 47 | {"0:0.001", 0, true}, 48 | {"0:0:.001", 0, true}, 49 | {"0:0:0.001", time.Millisecond, false}, 50 | {"0:0:-1.001", 0, true}, 51 | {"-1:0:0.001", 0, true}, 52 | {"0:-1:0.001", 0, true}, 53 | {"0:0:0.-001", 0, true}, 54 | {"0:1:61.001", 2*time.Minute + time.Second + time.Millisecond, false}, 55 | } { 56 | result, err := txt.ParseDuration(one.Input) 57 | desc := fmt.Sprintf("Index %d: %s", i, one.Input) 58 | if one.ExpectErr { 59 | check.Error(t, err, desc) 60 | } else { 61 | check.NoError(t, err, desc) 62 | check.Equal(t, one.ExpectedDuration, result, desc) 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /txt/emoji.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | package txt 11 | 12 | import "regexp" 13 | 14 | // SelectEmojiRegex identifies emoji runs. 15 | var SelectEmojiRegex = regexp.MustCompile(`[\x{200D}\x{203C}\x{2049}\x{20E3}\x{2122}\x{2139}\x{2194}-\x{2199}` + 16 | `\x{21A9}-\x{21AA}\x{231A}-\x{231B}\x{2328}\x{2388}\x{23CF}\x{23E9}-\x{23F3}\x{23F8}-\x{23FA}\x{24C2}` + 17 | `\x{25AA}-\x{25AB}\x{25B6}\x{25C0}\x{25FB}-\x{25FE}\x{2600}-\x{2605}\x{2607}-\x{2612}\x{2614}-\x{2705}` + 18 | `\x{2708}-\x{2712}\x{2714}\x{2716}\x{271D}\x{2721}\x{2728}\x{2733}-\x{2734}\x{2744}\x{2747}\x{274C}\x{274E}` + 19 | `\x{2753}-\x{2755}\x{2757}\x{2763}-\x{2767}\x{2795}-\x{2797}\x{27A1}\x{27B0}\x{27BF}\x{2934}-\x{2935}` + 20 | `\x{2B05}-\x{2B07}\x{2B1B}-\x{2B1C}\x{2B50}\x{2B55}\x{3030}\x{303D}\x{3297}\x{3299}\x{FE00}-\x{FE0F}` + 21 | `\x{1F000}-\x{1F0FF}\x{1F10D}-\x{1F10F}\x{1F12F}\x{1F16C}-\x{1F171}\x{1F17E}-\x{1F17F}\x{1F18E}` + 22 | `\x{1F191}-\x{1F19A}\x{1F1AD}-\x{1F1FF}\x{1F201}-\x{1F20F}\x{1F21A}\x{1F22F}\x{1F232}-\x{1F23A}` + 23 | `\x{1F23C}-\x{1F23F}\x{1F249}-\x{1F62B}\x{1F62C}-\x{1F64F}\x{1F680}-\x{1F6FF}\x{1F774}-\x{1F77F}` + 24 | `\x{1F7D5}-\x{1F7FF}\x{1F80C}-\x{1F80F}\x{1F848}-\x{1F84F}\x{1F85A}-\x{1F85F}\x{1F888}-\x{1F88F}` + 25 | `\x{1F8AE}-\x{1F93A}\x{1F93C}-\x{1F945}\x{1F947}-\x{1F9FF}\x{E0020}-\x{E007F}]`) 26 | -------------------------------------------------------------------------------- /txt/normalize.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | package txt 11 | 12 | import "strings" 13 | 14 | // NormalizeLineEndings converts CRLF and CR into LF. 15 | func NormalizeLineEndings(input string) string { 16 | return strings.ReplaceAll(strings.ReplaceAll(input, "\r\n", "\n"), "\r", "\n") 17 | } 18 | -------------------------------------------------------------------------------- /txt/roman.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | package txt 11 | 12 | import ( 13 | "strings" 14 | ) 15 | 16 | var ( 17 | romanValues = []int{1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1} 18 | romanText = []string{"M", "CM", "D", "CD", "C", "XC", "L", "XL", "X", "IX", "V", "IV", "I"} 19 | ) 20 | 21 | // RomanNumerals converts a number into roman numerals. 22 | func RomanNumerals(value int) string { 23 | var buffer strings.Builder 24 | for value > 0 { 25 | for i, v := range romanValues { 26 | if value >= v { 27 | buffer.WriteString(romanText[i]) 28 | value -= v 29 | break 30 | } 31 | } 32 | } 33 | return buffer.String() 34 | } 35 | -------------------------------------------------------------------------------- /txt/roman_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | package txt_test 11 | 12 | import ( 13 | "testing" 14 | 15 | "github.com/richardwilkes/toolbox/check" 16 | "github.com/richardwilkes/toolbox/txt" 17 | ) 18 | 19 | func TestToRoman(t *testing.T) { 20 | type data struct { 21 | e string 22 | v int 23 | } 24 | for _, one := range []data{ 25 | {v: 1, e: "I"}, 26 | {v: 2, e: "II"}, 27 | {v: 3, e: "III"}, 28 | {v: 4, e: "IV"}, 29 | {v: 5, e: "V"}, 30 | {v: 6, e: "VI"}, 31 | {v: 7, e: "VII"}, 32 | {v: 8, e: "VIII"}, 33 | {v: 9, e: "IX"}, 34 | {v: 10, e: "X"}, 35 | {v: 11, e: "XI"}, 36 | {v: 14, e: "XIV"}, 37 | {v: 39, e: "XXXIX"}, 38 | {v: 40, e: "XL"}, 39 | {v: 41, e: "XLI"}, 40 | {v: 49, e: "XLIX"}, 41 | {v: 50, e: "L"}, 42 | {v: 51, e: "LI"}, 43 | {v: 89, e: "LXXXIX"}, 44 | {v: 90, e: "XC"}, 45 | {v: 99, e: "XCIX"}, 46 | {v: 100, e: "C"}, 47 | {v: 399, e: "CCCXCIX"}, 48 | {v: 400, e: "CD"}, 49 | {v: 499, e: "CDXCIX"}, 50 | {v: 500, e: "D"}, 51 | {v: 899, e: "DCCCXCIX"}, 52 | {v: 900, e: "CM"}, 53 | {v: 1967, e: "MCMLXVII"}, 54 | {v: 2021, e: "MMXXI"}, 55 | } { 56 | check.Equal(t, one.e, txt.RomanNumerals(one.v), "input: %d", one.v) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /txt/rune_reader.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | package txt 11 | 12 | import "io" 13 | 14 | // RuneReader implements io.RuneReader 15 | type RuneReader struct { 16 | Src []rune 17 | Pos int 18 | } 19 | 20 | // ReadRune returns the next rune and its size in bytes. 21 | func (rr *RuneReader) ReadRune() (r rune, size int, err error) { 22 | if rr.Pos >= len(rr.Src) { 23 | return -1, 0, io.EOF 24 | } 25 | nextRune := rr.Src[rr.Pos] 26 | rr.Pos++ 27 | return nextRune, 1, nil 28 | } 29 | -------------------------------------------------------------------------------- /txt/slices.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | package txt 11 | 12 | import ( 13 | "strings" 14 | 15 | "github.com/richardwilkes/toolbox/collection/dict" 16 | ) 17 | 18 | // StringSliceToMap returns a map created from the strings of a slice. 19 | func StringSliceToMap(slice []string) map[string]bool { 20 | m := make(map[string]bool, len(slice)) 21 | for _, str := range slice { 22 | m[str] = true 23 | } 24 | return m 25 | } 26 | 27 | // MapToStringSlice returns a slice created from the keys of a map. 28 | // 29 | // Deprecated: Use dict.Keys instead. This function was deprecated on May 3, 2024 and will be removed on or after 30 | // January 1, 2025. 31 | func MapToStringSlice(m map[string]bool) []string { 32 | return dict.Keys(m) 33 | } 34 | 35 | // CloneStringSlice returns a copy of the slice of strings. 36 | func CloneStringSlice(in []string) []string { 37 | if len(in) == 0 { 38 | return nil 39 | } 40 | out := make([]string, len(in)) 41 | copy(out, in) 42 | return out 43 | } 44 | 45 | // RunesEqual returns true if the two slices of runes are equal. 46 | func RunesEqual(left, right []rune) bool { 47 | if len(left) != len(right) { 48 | return false 49 | } 50 | for i := range left { 51 | if left[i] != right[i] { 52 | return false 53 | } 54 | } 55 | return true 56 | } 57 | 58 | // CaselessSliceContains returns true if the target is within the slice, regardless of case. 59 | func CaselessSliceContains(slice []string, target string) bool { 60 | for _, one := range slice { 61 | if strings.EqualFold(one, target) { 62 | return true 63 | } 64 | } 65 | return false 66 | } 67 | -------------------------------------------------------------------------------- /txt/substr.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | package txt 11 | 12 | // FirstN returns the first n runes of a string. 13 | func FirstN(s string, n int) string { 14 | if n < 1 { 15 | return "" 16 | } 17 | r := []rune(s) 18 | if n > len(r) { 19 | return s 20 | } 21 | return string(r[:n]) 22 | } 23 | 24 | // LastN returns the last n runes of a string. 25 | func LastN(s string, n int) string { 26 | if n < 1 { 27 | return "" 28 | } 29 | r := []rune(s) 30 | if n > len(r) { 31 | return s 32 | } 33 | return string(r[len(r)-n:]) 34 | } 35 | 36 | // Truncate the input string to count runes, trimming from the end if keepFirst is true or the start if not. If trimming 37 | // occurs, a … will be added in place of the trimmed characters. 38 | func Truncate(s string, count int, keepFirst bool) string { 39 | var result string 40 | if keepFirst { 41 | result = FirstN(s, count) 42 | } else { 43 | result = LastN(s, count) 44 | } 45 | if result != s { 46 | if keepFirst { 47 | result += "…" 48 | } else { 49 | result = "…" + result 50 | } 51 | } 52 | return result 53 | } 54 | -------------------------------------------------------------------------------- /txt/substr_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | package txt_test 11 | 12 | import ( 13 | "testing" 14 | 15 | "github.com/richardwilkes/toolbox/check" 16 | "github.com/richardwilkes/toolbox/txt" 17 | ) 18 | 19 | func TestFirstN(t *testing.T) { 20 | table := []struct { 21 | In string 22 | Out string 23 | N int 24 | }{ 25 | {In: "abcd", N: 3, Out: "abc"}, 26 | {In: "abcd", N: 5, Out: "abcd"}, 27 | {In: "abcd", N: 0, Out: ""}, 28 | {In: "abcd", N: -1, Out: ""}, 29 | {In: "aécd", N: 3, Out: "aéc"}, 30 | {In: "aécd", N: 5, Out: "aécd"}, 31 | {In: "aécd", N: 0, Out: ""}, 32 | {In: "aécd", N: -1, Out: ""}, 33 | } 34 | for i, one := range table { 35 | check.Equal(t, one.Out, txt.FirstN(one.In, one.N), "#%d", i) 36 | } 37 | } 38 | 39 | func TestLastN(t *testing.T) { 40 | table := []struct { 41 | In string 42 | Out string 43 | N int 44 | }{ 45 | {In: "abcd", N: 3, Out: "bcd"}, 46 | {In: "abcd", N: 5, Out: "abcd"}, 47 | {In: "abcd", N: 0, Out: ""}, 48 | {In: "abcd", N: -1, Out: ""}, 49 | {In: "aécd", N: 3, Out: "écd"}, 50 | {In: "aécd", N: 5, Out: "aécd"}, 51 | {In: "aécd", N: 0, Out: ""}, 52 | {In: "aécd", N: -1, Out: ""}, 53 | } 54 | for i, one := range table { 55 | check.Equal(t, one.Out, txt.LastN(one.In, one.N), "#%d", i) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /txt/truthy.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | package txt 11 | 12 | import "strings" 13 | 14 | // IsTruthy returns true for "truthy" values, i.e. ones that should be interpreted as true. 15 | func IsTruthy(in string) bool { 16 | in = strings.ToLower(in) 17 | return in == "1" || in == "true" || in == "yes" || in == "on" 18 | } 19 | -------------------------------------------------------------------------------- /txt/unicode.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | package txt 11 | 12 | // StripBOM removes the BOM marker from UTF-8 data, if present. 13 | func StripBOM(b []byte) []byte { 14 | if len(b) >= 3 && b[0] == 0xef && b[1] == 0xbb && b[2] == 0xbf { 15 | return b[3:] 16 | } 17 | return b 18 | } 19 | -------------------------------------------------------------------------------- /txt/unquote.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | package txt 11 | 12 | import ( 13 | "unicode/utf8" 14 | ) 15 | 16 | // UnquoteBytes strips up to one set of surrounding double quotes from the bytes and returns them as a string. For a 17 | // more capable version that supports different quoting types and unescaping, consider using strconv.Unquote(). 18 | func UnquoteBytes(text []byte) []byte { 19 | if len(text) > 1 { 20 | if ch, _ := utf8.DecodeRune(text); ch == '"' { 21 | if ch, _ = utf8.DecodeLastRune(text); ch == '"' { 22 | text = text[1 : len(text)-1] 23 | } 24 | } 25 | } 26 | return text 27 | } 28 | 29 | // Unquote strips up to one set of surrounding double quotes from the bytes and returns them as a string. For a more 30 | // capable version that supports different quoting types and unescaping, consider using strconv.Unquote(). 31 | func Unquote(text string) string { 32 | if len(text) > 1 { 33 | if ch, _ := utf8.DecodeRuneInString(text); ch == '"' { 34 | if ch, _ = utf8.DecodeLastRuneInString(text); ch == '"' { 35 | text = text[1 : len(text)-1] 36 | } 37 | } 38 | } 39 | return text 40 | } 41 | -------------------------------------------------------------------------------- /txt/vowel.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | package txt 11 | 12 | import "unicode" 13 | 14 | // VowelChecker defines a function that returns true if the specified rune is to be considered a vowel. 15 | type VowelChecker func(rune) bool 16 | 17 | // IsVowel is a concrete implementation of VowelChecker. 18 | func IsVowel(ch rune) bool { 19 | if unicode.IsUpper(ch) { 20 | ch = unicode.ToLower(ch) 21 | } 22 | switch ch { 23 | case 'a', 'à', 'á', 'â', 'ä', 'æ', 'ã', 'å', 'ā', 24 | 'e', 'è', 'é', 'ê', 'ë', 'ē', 'ė', 'ę', 25 | 'i', 'î', 'ï', 'í', 'ī', 'į', 'ì', 26 | 'o', 'ô', 'ö', 'ò', 'ó', 'œ', 'ø', 'ō', 'õ', 27 | 'u', 'û', 'ü', 'ù', 'ú', 'ū': 28 | return true 29 | default: 30 | return false 31 | } 32 | } 33 | 34 | // IsVowely is a concrete implementation of VowelChecker that includes 'y'. 35 | func IsVowely(ch rune) bool { 36 | if unicode.IsUpper(ch) { 37 | ch = unicode.ToLower(ch) 38 | } 39 | switch ch { 40 | case 'y', 'ÿ': 41 | return true 42 | default: 43 | return IsVowel(ch) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /txt/wrap.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | package txt 11 | 12 | import ( 13 | "strings" 14 | ) 15 | 16 | // Wrap text to a certain length, giving it an optional prefix on each line. Words will not be broken, even if they 17 | // exceed the maximum column size and instead will extend past the desired length. 18 | func Wrap(prefix, text string, maxColumns int) string { 19 | var buffer strings.Builder 20 | for i, line := range strings.Split(text, "\n") { 21 | if i != 0 { 22 | buffer.WriteByte('\n') 23 | } 24 | buffer.WriteString(prefix) 25 | avail := maxColumns - len(prefix) 26 | for j, token := range strings.Fields(line) { 27 | if j != 0 { 28 | if 1+len(token) > avail { 29 | buffer.WriteByte('\n') 30 | buffer.WriteString(prefix) 31 | avail = maxColumns - len(prefix) 32 | } else { 33 | buffer.WriteByte(' ') 34 | avail-- 35 | } 36 | } 37 | buffer.WriteString(token) 38 | avail -= len(token) 39 | } 40 | } 41 | return buffer.String() 42 | } 43 | -------------------------------------------------------------------------------- /txt/wrap_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | package txt_test 11 | 12 | import ( 13 | "testing" 14 | 15 | "github.com/richardwilkes/toolbox/check" 16 | "github.com/richardwilkes/toolbox/txt" 17 | ) 18 | 19 | func TestWrap(t *testing.T) { 20 | table := []struct { 21 | Prefix string 22 | Text string 23 | Out string 24 | Max int 25 | }{ 26 | {Prefix: "// ", Text: "short", Max: 78, Out: "// short"}, 27 | {Prefix: "// ", Text: "some text that is longer", Max: 12, Out: "// some text\n// that is\n// longer"}, 28 | {Prefix: "// ", Text: "some text\nwith embedded line feeds", Max: 16, Out: "// some text\n// with embedded\n// line feeds"}, 29 | {Prefix: "", Text: "some text that is longer", Max: 12, Out: "some text\nthat is\nlonger"}, 30 | {Prefix: "", Text: "some text that is longer", Max: 4, Out: "some\ntext\nthat\nis\nlonger"}, 31 | {Prefix: "", Text: "some text that is longer, yep", Max: 4, Out: "some\ntext\nthat\nis\nlonger,\nyep"}, 32 | {Prefix: "", Text: "some text\nwith embedded line feeds", Max: 16, Out: "some text\nwith embedded\nline feeds"}, 33 | } 34 | for i, one := range table { 35 | check.Equal(t, one.Out, txt.Wrap(one.Prefix, one.Text, one.Max), "#%d", i) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /user.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | package toolbox 11 | 12 | import ( 13 | "cmp" 14 | "os" 15 | "os/user" 16 | ) 17 | 18 | // CurrentUserName returns the current user's name. This will attempt to retrieve the user's display name, but will fall 19 | // back to the account name if it isn't available. 20 | func CurrentUserName() string { 21 | if u, err := user.Current(); err == nil { 22 | return cmp.Or(u.Name, u.Username) 23 | } 24 | return os.Getenv("USER") 25 | } 26 | -------------------------------------------------------------------------------- /xcrypto/stream.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | package xcrypto 11 | 12 | import ( 13 | "crypto/aes" 14 | "crypto/cipher" 15 | "crypto/rand" 16 | "crypto/rsa" 17 | "crypto/sha256" 18 | "io" 19 | 20 | "github.com/richardwilkes/toolbox/errs" 21 | ) 22 | 23 | // EncryptStreamWithPublicKey copies 'in' to 'out', encrypting the bytes along the way. Note that the output stream will 24 | // be larger than the input stream by aes.BlockSize + publicKey.Size() bytes. 25 | func EncryptStreamWithPublicKey(in io.Reader, out io.Writer, publicKey *rsa.PublicKey) error { 26 | iv := make([]byte, aes.BlockSize) 27 | if _, err := io.ReadFull(rand.Reader, iv); err != nil { 28 | return errs.Wrap(err) 29 | } 30 | encryptionKey := make([]byte, 32) // aes256 31 | if _, err := io.ReadFull(rand.Reader, encryptionKey); err != nil { 32 | return errs.Wrap(err) 33 | } 34 | encryptedEncryptionKey, err := rsa.EncryptOAEP(sha256.New(), rand.Reader, publicKey, encryptionKey, nil) 35 | if err != nil { 36 | return errs.Wrap(err) 37 | } 38 | block, err := aes.NewCipher(encryptionKey) 39 | if err != nil { 40 | return errs.Wrap(err) 41 | } 42 | if _, err = out.Write(encryptedEncryptionKey); err != nil { 43 | return errs.Wrap(err) 44 | } 45 | if _, err = out.Write(iv); err != nil { 46 | return errs.Wrap(err) 47 | } 48 | if _, err = io.Copy(&cipher.StreamWriter{ 49 | S: cipher.NewCTR(block, iv), 50 | W: out, 51 | }, in); err != nil { 52 | return errs.Wrap(err) 53 | } 54 | return nil 55 | } 56 | 57 | // DecryptStreamWithPrivateKey copies 'in' to 'out', decrypting the bytes along the way. Note that the output stream 58 | // will be smaller than the input stream by aes.BlockSize + publicKey.Size() bytes. 59 | func DecryptStreamWithPrivateKey(in io.Reader, out io.Writer, privateKey *rsa.PrivateKey) error { 60 | encryptedEncryptionKey := make([]byte, privateKey.Size()) 61 | if _, err := in.Read(encryptedEncryptionKey); err != nil { 62 | return errs.Wrap(err) 63 | } 64 | iv := make([]byte, aes.BlockSize) 65 | if _, err := in.Read(iv); err != nil { 66 | return errs.Wrap(err) 67 | } 68 | encryptionKey, err := rsa.DecryptOAEP(sha256.New(), rand.Reader, privateKey, encryptedEncryptionKey, nil) 69 | if err != nil { 70 | return errs.Wrap(err) 71 | } 72 | block, err := aes.NewCipher(encryptionKey) 73 | if err != nil { 74 | return errs.Wrap(err) 75 | } 76 | if _, err = io.Copy(out, &cipher.StreamReader{ 77 | S: cipher.NewCTR(block, iv), 78 | R: in, 79 | }); err != nil { 80 | return errs.Wrap(err) 81 | } 82 | return nil 83 | } 84 | -------------------------------------------------------------------------------- /xio/bom_stripper.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | package xio 11 | 12 | import ( 13 | "bufio" 14 | "io" 15 | 16 | "github.com/richardwilkes/toolbox/errs" 17 | ) 18 | 19 | const utf8BOM = '\uFEFF' 20 | 21 | // NewBOMStripper strips a leading UTF-8 BOM marker from the input. The reader that is returned will be the same as the 22 | // one passed in if it was a *bufio.Reader, otherwise, the original reader will be wrapped with a *bufio.Reader and 23 | // returned. 24 | func NewBOMStripper(r io.Reader) (*bufio.Reader, error) { 25 | buffer, ok := r.(*bufio.Reader) 26 | if !ok { 27 | buffer = bufio.NewReader(r) 28 | } 29 | ch, _, err := buffer.ReadRune() 30 | if err != nil { 31 | return nil, errs.Wrap(err) 32 | } 33 | if ch != utf8BOM { 34 | if err = buffer.UnreadRune(); err != nil { 35 | return nil, errs.Wrap(err) 36 | } 37 | } 38 | return buffer, nil 39 | } 40 | -------------------------------------------------------------------------------- /xio/close.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | // Package xio provides i/o utilities. 11 | package xio 12 | 13 | import ( 14 | "io" 15 | "log/slog" 16 | 17 | "github.com/richardwilkes/toolbox/errs" 18 | ) 19 | 20 | // CloseIgnoringErrors closes the closer and ignores any error it might produce. Should only be used for read-only 21 | // streams of data where closing should never cause an error. 22 | func CloseIgnoringErrors(closer io.Closer) { 23 | _ = closer.Close() //nolint:errcheck // intentionally ignoring any error 24 | } 25 | 26 | // DiscardAndCloseIgnoringErrors reads any content remaining in the body and discards it, then closes the body. 27 | func DiscardAndCloseIgnoringErrors(rc io.ReadCloser) { 28 | _, _ = io.Copy(io.Discard, rc) //nolint:errcheck // intentionally ignoring any error 29 | CloseIgnoringErrors(rc) 30 | } 31 | 32 | // CloseLoggingAnyError closes the closer and logs any error that occurs at an error level to the default logger. 33 | func CloseLoggingAnyError(closer io.Closer) { 34 | CloseLoggingAnyErrorTo(slog.Default(), closer) 35 | } 36 | 37 | // CloseLoggingAnyErrorTo closes the closer and logs any error that occurs to the provided logger. 38 | func CloseLoggingAnyErrorTo(logger *slog.Logger, closer io.Closer) { 39 | if err := closer.Close(); err != nil { 40 | errs.LogTo(logger, err) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /xio/context.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | package xio 11 | 12 | import ( 13 | "context" 14 | "time" 15 | 16 | "github.com/richardwilkes/toolbox/errs" 17 | ) 18 | 19 | // Contexter is an interface that provides a context. 20 | type Contexter interface { 21 | Context() context.Context 22 | } 23 | 24 | // ContextSleep sleeps for the specified time, or until the context is done. You can check the return error to see if 25 | // the context deadline was exceeded by using errors.Is(err, context.DeadlineExceeded). 26 | func ContextSleep(ctx context.Context, waitTime time.Duration) error { 27 | timer := time.NewTimer(waitTime) 28 | defer timer.Stop() 29 | select { 30 | case <-ctx.Done(): 31 | err := ctx.Err() 32 | return errs.NewWithCause(err.Error(), err) 33 | case <-timer.C: 34 | return nil 35 | } 36 | } 37 | 38 | // ContexterWasCanceled checks the context held by the contexter to see if it was canceled. 39 | func ContexterWasCanceled(ctxer Contexter) bool { 40 | return ContextWasCanceled(ctxer.Context()) 41 | } 42 | 43 | // ContextWasCanceled checks the context to see if it was canceled. 44 | func ContextWasCanceled(ctx context.Context) bool { 45 | select { 46 | case <-ctx.Done(): 47 | if ctx.Err() != nil { 48 | return true 49 | } 50 | default: 51 | } 52 | return false 53 | } 54 | -------------------------------------------------------------------------------- /xio/fs/copy.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | // Package fs provides filesystem-related utilities. 11 | package fs 12 | 13 | import ( 14 | "io" 15 | "io/fs" 16 | "os" 17 | "path/filepath" 18 | 19 | "github.com/richardwilkes/toolbox/xio" 20 | ) 21 | 22 | // Copy src to dst. src may be a directory, file, or symlink. 23 | func Copy(src, dst string) error { 24 | return CopyWithMask(src, dst, 0o777) 25 | } 26 | 27 | // CopyWithMask src to dst. src may be a directory, file, or symlink. 28 | func CopyWithMask(src, dst string, mask fs.FileMode) error { 29 | info, err := os.Lstat(src) 30 | if err != nil { 31 | return err 32 | } 33 | return generalCopy(src, dst, info.Mode(), mask) 34 | } 35 | 36 | func generalCopy(src, dst string, srcMode, mask fs.FileMode) error { 37 | if srcMode&os.ModeSymlink != 0 { 38 | return linkCopy(src, dst) 39 | } 40 | if srcMode.IsDir() { 41 | return dirCopy(src, dst, srcMode, mask) 42 | } 43 | return fileCopy(src, dst, srcMode, mask) 44 | } 45 | 46 | func fileCopy(src, dst string, srcMode, mask fs.FileMode) (err error) { 47 | if err = os.MkdirAll(filepath.Dir(dst), 0o755&mask); err != nil { 48 | return err 49 | } 50 | var f *os.File 51 | if f, err = os.OpenFile(dst, os.O_RDWR|os.O_CREATE|os.O_TRUNC, (srcMode&mask)|0o200); err != nil { 52 | return err 53 | } 54 | defer func() { 55 | if closeErr := f.Close(); closeErr != nil && err == nil { 56 | err = closeErr 57 | } 58 | }() 59 | var s *os.File 60 | if s, err = os.Open(src); err != nil { 61 | return err 62 | } 63 | defer xio.CloseIgnoringErrors(s) 64 | _, err = io.Copy(f, s) 65 | return err 66 | } 67 | 68 | func dirCopy(srcDir, dstDir string, srcMode, mask fs.FileMode) error { 69 | if err := os.MkdirAll(dstDir, srcMode&mask); err != nil { 70 | return err 71 | } 72 | list, err := os.ReadDir(srcDir) 73 | if err != nil { 74 | return err 75 | } 76 | for _, one := range list { 77 | name := one.Name() 78 | if err = generalCopy(filepath.Join(srcDir, name), filepath.Join(dstDir, name), one.Type(), mask); err != nil { 79 | return err 80 | } 81 | } 82 | return nil 83 | } 84 | 85 | func linkCopy(src, dst string) error { 86 | s, err := os.Readlink(src) 87 | if err != nil { 88 | return err 89 | } 90 | return os.Symlink(s, dst) 91 | } 92 | -------------------------------------------------------------------------------- /xio/fs/dir.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | package fs 11 | 12 | import "os" 13 | 14 | // IsDir returns true if the specified path exists and is a directory. 15 | func IsDir(path string) bool { 16 | fi, err := os.Stat(path) 17 | return err == nil && fi.IsDir() 18 | } 19 | -------------------------------------------------------------------------------- /xio/fs/exists.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | package fs 11 | 12 | import "os" 13 | 14 | // FileExists returns true if the path points to a regular file. 15 | func FileExists(path string) bool { 16 | if fi, err := os.Stat(path); err == nil { 17 | mode := fi.Mode() 18 | return !mode.IsDir() && mode.IsRegular() 19 | } 20 | return false 21 | } 22 | -------------------------------------------------------------------------------- /xio/fs/filename.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | package fs 11 | 12 | import ( 13 | "path/filepath" 14 | "strings" 15 | ) 16 | 17 | // SanitizeName sanitizes a file name by replacing invalid characters. 18 | func SanitizeName(name string) string { 19 | if name == "" { 20 | return "@0" 21 | } 22 | if name == "." { 23 | return "@1" 24 | } 25 | if name == ".." { 26 | return "@2" 27 | } 28 | var buffer strings.Builder 29 | for _, r := range name { 30 | switch r { 31 | case '@': 32 | buffer.WriteString("@3") 33 | case '/': 34 | buffer.WriteString("@4") 35 | case '\\': 36 | buffer.WriteString("@5") 37 | case ':': 38 | buffer.WriteString("@6") 39 | default: 40 | buffer.WriteRune(r) 41 | } 42 | } 43 | return buffer.String() 44 | } 45 | 46 | // UnsanitizeName reverses the effects of a call to SanitizeName. 47 | func UnsanitizeName(name string) string { 48 | if name == "@0" { 49 | return "" 50 | } 51 | if name == "@1" { 52 | return "." 53 | } 54 | if name == "@2" { 55 | return ".." 56 | } 57 | var buffer strings.Builder 58 | found := false 59 | for _, r := range name { 60 | switch { 61 | case found: 62 | switch r { 63 | case '3': 64 | buffer.WriteByte('@') 65 | case '4': 66 | buffer.WriteByte('/') 67 | case '5': 68 | buffer.WriteByte('\\') 69 | case '6': 70 | buffer.WriteByte(':') 71 | default: 72 | buffer.WriteByte('@') 73 | buffer.WriteRune(r) 74 | } 75 | found = false 76 | case r == '@': 77 | found = true 78 | default: 79 | buffer.WriteRune(r) 80 | } 81 | } 82 | if found { 83 | buffer.WriteByte('@') 84 | } 85 | return buffer.String() 86 | } 87 | 88 | // BaseName returns the file name without the directory or extension. 89 | func BaseName(path string) string { 90 | return TrimExtension(filepath.Base(path)) 91 | } 92 | 93 | // TrimExtension trims any extension from the path. 94 | func TrimExtension(path string) string { 95 | return path[:len(path)-len(filepath.Ext(path))] 96 | } 97 | -------------------------------------------------------------------------------- /xio/fs/internal/temp.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | package internal 11 | 12 | import ( 13 | "errors" 14 | "math" 15 | "os" 16 | "path/filepath" 17 | "strconv" 18 | "strings" 19 | 20 | "github.com/richardwilkes/toolbox/xmath/rand" 21 | ) 22 | 23 | // CreateTemp is essentially the same as os.CreateTemp, except it allows you to specify the file mode of the newly 24 | // created file. This is here solely because having it in the fs package would cause circular references. 25 | func CreateTemp(dir, pattern string, perm os.FileMode) (*os.File, error) { 26 | if dir == "" { 27 | dir = os.TempDir() 28 | } 29 | for i := range len(pattern) { 30 | if os.IsPathSeparator(pattern[i]) { 31 | return nil, &os.PathError{Op: "createtemp", Path: pattern, Err: errors.New("pattern contains path separator")} 32 | } 33 | } 34 | var prefix, suffix string 35 | if pos := strings.LastIndexByte(pattern, '*'); pos != -1 { 36 | prefix, suffix = pattern[:pos], pattern[pos+1:] 37 | } else { 38 | prefix = pattern 39 | } 40 | if dir != "" && os.IsPathSeparator(dir[len(dir)-1]) { 41 | prefix = dir + prefix 42 | } else { 43 | prefix = filepath.Join(dir, prefix) 44 | } 45 | try := 0 46 | for { 47 | f, err := os.OpenFile(prefix+strconv.Itoa(rand.NewCryptoRand().Intn(math.MaxInt))+suffix, 48 | os.O_RDWR|os.O_CREATE|os.O_EXCL, perm) 49 | if os.IsExist(err) { 50 | try++ 51 | if try < 1000 { 52 | continue 53 | } 54 | return nil, &os.PathError{Op: "createtemp", Path: prefix + "*" + suffix, Err: os.ErrExist} 55 | } 56 | return f, err 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /xio/fs/json.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | package fs 11 | 12 | import ( 13 | "bufio" 14 | "encoding/json" 15 | "io" 16 | "io/fs" 17 | "os" 18 | 19 | "github.com/richardwilkes/toolbox/errs" 20 | "github.com/richardwilkes/toolbox/xio" 21 | "github.com/richardwilkes/toolbox/xio/fs/safe" 22 | ) 23 | 24 | // LoadJSON data from the specified path. 25 | func LoadJSON(path string, data any) error { 26 | f, err := os.Open(path) 27 | if err != nil { 28 | return errs.NewWithCause(path, err) 29 | } 30 | return loadJSON(f, path, data) 31 | } 32 | 33 | // LoadJSONFromFS data from the specified filesystem path. 34 | func LoadJSONFromFS(fsys fs.FS, path string, data any) error { 35 | f, err := fsys.Open(path) 36 | if err != nil { 37 | return errs.NewWithCause(path, err) 38 | } 39 | return loadJSON(f, path, data) 40 | } 41 | 42 | func loadJSON(r io.ReadCloser, path string, data any) error { 43 | defer xio.CloseIgnoringErrors(r) 44 | if err := json.NewDecoder(bufio.NewReader(r)).Decode(data); err != nil { 45 | return errs.NewWithCause(path, err) 46 | } 47 | return nil 48 | } 49 | 50 | // SaveJSON data to the specified path. 51 | func SaveJSON(path string, data any, format bool) error { 52 | return SaveJSONWithMode(path, data, format, 0o644) 53 | } 54 | 55 | // SaveJSONWithMode data to the specified path. 56 | func SaveJSONWithMode(path string, data any, format bool, mode os.FileMode) error { 57 | if err := safe.WriteFileWithMode(path, func(w io.Writer) error { 58 | encoder := json.NewEncoder(w) 59 | if format { 60 | encoder.SetIndent("", " ") 61 | } 62 | return errs.Wrap(encoder.Encode(data)) 63 | }, mode); err != nil { 64 | return errs.NewWithCause(path, err) 65 | } 66 | return nil 67 | } 68 | -------------------------------------------------------------------------------- /xio/fs/json_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | package fs_test 11 | 12 | import ( 13 | "os" 14 | "testing" 15 | 16 | "github.com/richardwilkes/toolbox/check" 17 | "github.com/richardwilkes/toolbox/xio/fs" 18 | ) 19 | 20 | func TestLoadSaveJSON(t *testing.T) { 21 | type data struct { 22 | Name string 23 | Count int 24 | } 25 | value := &data{ 26 | Name: "Rich", 27 | Count: 22, 28 | } 29 | f, err := os.CreateTemp("", "json_test") 30 | check.NoError(t, err) 31 | check.NoError(t, f.Close()) 32 | check.NoError(t, fs.SaveJSONWithMode(f.Name(), value, false, 0o600)) 33 | var value2 data 34 | check.NoError(t, fs.LoadJSON(f.Name(), &value2)) 35 | check.NoError(t, os.Remove(f.Name())) 36 | check.Equal(t, value, &value2) 37 | } 38 | -------------------------------------------------------------------------------- /xio/fs/move.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | package fs 11 | 12 | import ( 13 | "io" 14 | "os" 15 | 16 | "github.com/richardwilkes/toolbox/errs" 17 | "github.com/richardwilkes/toolbox/xio" 18 | ) 19 | 20 | // MoveFile moves a file in the file system or across volumes, using rename if possible, but falling back to copying the 21 | // file if not. This will error if either src or dst are not regular files. 22 | func MoveFile(src, dst string) (err error) { 23 | var srcInfo, dstInfo os.FileInfo 24 | srcInfo, err = os.Stat(src) 25 | if err != nil { 26 | return errs.Wrap(err) 27 | } 28 | if !srcInfo.Mode().IsRegular() { 29 | return errs.Newf("%s is not a regular file", src) 30 | } 31 | dstInfo, err = os.Stat(dst) 32 | if err != nil { 33 | if !os.IsNotExist(err) { 34 | return errs.Wrap(err) 35 | } 36 | } else { 37 | if !dstInfo.Mode().IsRegular() { 38 | return errs.Newf("%s is not a regular file", dst) 39 | } 40 | if os.SameFile(srcInfo, dstInfo) { 41 | return nil 42 | } 43 | } 44 | if os.Rename(src, dst) == nil { 45 | return nil 46 | } 47 | var in, out *os.File 48 | out, err = os.OpenFile(dst, os.O_RDWR|os.O_CREATE|os.O_TRUNC, srcInfo.Mode()) 49 | if err != nil { 50 | return errs.Wrap(err) 51 | } 52 | defer func() { 53 | if closeErr := out.Close(); closeErr != nil && err == nil { 54 | err = closeErr 55 | } 56 | }() 57 | if in, err = os.Open(src); err != nil { 58 | err = errs.Wrap(err) 59 | return 60 | } 61 | _, err = io.Copy(out, in) 62 | xio.CloseIgnoringErrors(in) 63 | if err != nil { 64 | err = errs.Wrap(err) 65 | return 66 | } 67 | if err = os.Remove(src); err != nil { 68 | err = errs.Wrap(err) 69 | } 70 | return 71 | } 72 | -------------------------------------------------------------------------------- /xio/fs/paths/paths.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | // Package paths provides platform-specific standard paths. 11 | package paths 12 | 13 | import ( 14 | "os" 15 | "os/user" 16 | ) 17 | 18 | // HomeDir returns the home directory. If this cannot be determined for some reason, "." will be returned instead. 19 | func HomeDir() string { 20 | if u, err := user.Current(); err == nil { 21 | return u.HomeDir 22 | } 23 | if dir, err := os.UserHomeDir(); err == nil { 24 | return dir 25 | } 26 | return "." 27 | } 28 | -------------------------------------------------------------------------------- /xio/fs/paths/paths_darwin.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | package paths 11 | 12 | import ( 13 | "path/filepath" 14 | 15 | "github.com/richardwilkes/toolbox/cmdline" 16 | ) 17 | 18 | // AppDataDir returns the application data directory. 19 | func AppDataDir() string { 20 | path := filepath.Join(HomeDir(), "Library", "Application Support") 21 | if cmdline.AppIdentifier != "" { 22 | path = filepath.Join(path, cmdline.AppIdentifier) 23 | } 24 | return path 25 | } 26 | 27 | // AppLogDir returns the application log directory. 28 | func AppLogDir() string { 29 | path := filepath.Join(HomeDir(), "Library", "Logs") 30 | if cmdline.AppIdentifier != "" { 31 | path = filepath.Join(path, cmdline.AppIdentifier) 32 | } 33 | return path 34 | } 35 | 36 | // FontDirs returns the standard font directories, in order of priority. 37 | func FontDirs() []string { 38 | return []string{filepath.Join(HomeDir(), "Library", "Fonts"), "/Library/Fonts", "/System/Library/Fonts"} 39 | } 40 | -------------------------------------------------------------------------------- /xio/fs/paths/paths_linux.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | package paths 11 | 12 | import ( 13 | "os" 14 | "path/filepath" 15 | 16 | "github.com/richardwilkes/toolbox/cmdline" 17 | ) 18 | 19 | // AppDataDir returns the application data directory. 20 | func AppDataDir() string { 21 | path := os.Getenv("XDG_DATA_HOME") 22 | if path == "" { 23 | path = filepath.Join(HomeDir(), ".local", "share") 24 | } 25 | if cmdline.AppIdentifier != "" { 26 | path = filepath.Join(path, cmdline.AppIdentifier) 27 | } 28 | return path 29 | } 30 | 31 | // AppLogDir returns the application log directory. 32 | func AppLogDir() string { 33 | return filepath.Join(AppDataDir(), "Logs") 34 | } 35 | 36 | // FontDirs returns the standard font directories, in order of priority. 37 | func FontDirs() []string { 38 | return []string{filepath.Join(HomeDir(), ".fonts"), "/usr/local/share/fonts", "/usr/share/fonts"} 39 | } 40 | -------------------------------------------------------------------------------- /xio/fs/paths/paths_windows.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | package paths 11 | 12 | import ( 13 | "os" 14 | "path/filepath" 15 | 16 | "github.com/richardwilkes/toolbox/cmdline" 17 | ) 18 | 19 | // AppDataDir returns the application data directory. 20 | func AppDataDir() string { 21 | path := filepath.Join(HomeDir(), "AppData", "Local") 22 | if cmdline.AppIdentifier != "" { 23 | path = filepath.Join(path, cmdline.AppIdentifier) 24 | } 25 | return path 26 | } 27 | 28 | // AppLogDir returns the application log directory. 29 | func AppLogDir() string { 30 | return filepath.Join(AppDataDir(), "Logs") 31 | } 32 | 33 | // FontDirs returns the standard font directories, in order of priority. 34 | func FontDirs() []string { 35 | windir := os.Getenv("WINDIR") 36 | if windir == "" { 37 | windir = `C:\Windows` 38 | } 39 | return []string{filepath.Join(windir, "Fonts")} 40 | } 41 | -------------------------------------------------------------------------------- /xio/fs/readable.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | package fs 11 | 12 | import "os" 13 | 14 | // FileIsReadable returns true if the path points to a regular file that we have permission to read. 15 | func FileIsReadable(path string) bool { 16 | if fi, err := os.Stat(path); err == nil { 17 | mode := fi.Mode() 18 | return !mode.IsDir() && mode.IsRegular() && mode.Perm()&0o400 != 0 19 | } 20 | return false 21 | } 22 | -------------------------------------------------------------------------------- /xio/fs/roots.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | package fs 11 | 12 | import ( 13 | "path/filepath" 14 | "strings" 15 | 16 | "github.com/richardwilkes/toolbox/errs" 17 | ) 18 | 19 | // UniquePaths returns a list of unique paths from the given paths, pruning out paths that are a subset of another. 20 | func UniquePaths(paths ...string) ([]string, error) { 21 | set := make(map[string]bool, len(paths)) 22 | for _, path := range paths { 23 | actual, err := filepath.Abs(path) 24 | if err != nil { 25 | return nil, errs.NewWithCause(path, err) 26 | } 27 | if actual, err = filepath.EvalSymlinks(actual); err != nil { 28 | return nil, errs.NewWithCause(path, err) 29 | } 30 | if _, exists := set[actual]; !exists { 31 | add := true 32 | for one := range set { 33 | var p1, p2 string 34 | if p1, err = filepath.Rel(one, actual); err != nil { 35 | return nil, errs.NewWithCause(path, err) 36 | } 37 | if p2, err = filepath.Rel(actual, one); err != nil { 38 | return nil, errs.NewWithCause(path, err) 39 | } 40 | prefixed := strings.HasPrefix(p1, "..") 41 | if prefixed != strings.HasPrefix(p2, "..") { 42 | if prefixed { 43 | delete(set, one) 44 | } else { 45 | add = false 46 | break 47 | } 48 | } 49 | } 50 | if add { 51 | set[actual] = true 52 | } 53 | } 54 | } 55 | result := make([]string, 0, len(set)) 56 | for p := range set { 57 | result = append(result, p) 58 | } 59 | return result, nil 60 | } 61 | -------------------------------------------------------------------------------- /xio/fs/safe/file.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | // Package safe provides safe, atomic saving of files. 11 | package safe 12 | 13 | import ( 14 | "os" 15 | "path/filepath" 16 | 17 | "github.com/richardwilkes/toolbox/xio/fs/internal" 18 | ) 19 | 20 | // File provides safe, atomic saving of files. Instead of truncating and overwriting the destination file, it creates a 21 | // temporary file in the same directory, writes to it, and then renames the temporary file to the original name when 22 | // Commit() is called. If Close() is called without calling Commit(), or the Commit() fails, then the original file is 23 | // left untouched. 24 | type File struct { 25 | *os.File 26 | originalName string 27 | committed bool 28 | closed bool 29 | } 30 | 31 | // Create creates a temporary file in the same directory as filename, which will be renamed to the given filename when 32 | // calling Commit. 33 | func Create(filename string) (*File, error) { 34 | return CreateWithMode(filename, 0o644) 35 | } 36 | 37 | // CreateWithMode creates a temporary file in the same directory as filename, which will be renamed to the given 38 | // filename when calling Commit. 39 | func CreateWithMode(filename string, mode os.FileMode) (*File, error) { 40 | filename = filepath.Clean(filename) 41 | if filename == "" || filename[len(filename)-1] == filepath.Separator { 42 | return nil, os.ErrInvalid 43 | } 44 | f, err := internal.CreateTemp(filepath.Dir(filename), "safe", mode) 45 | if err != nil { 46 | return nil, err 47 | } 48 | return &File{ 49 | File: f, 50 | originalName: filename, 51 | }, nil 52 | } 53 | 54 | // OriginalName returns the original filename passed into Create(). 55 | func (f *File) OriginalName() string { 56 | return f.originalName 57 | } 58 | 59 | // Commit the data into the original file and remove the temporary file from disk. Close() may still be called, but will 60 | // do nothing. 61 | func (f *File) Commit() error { 62 | if f.committed { 63 | return nil 64 | } 65 | if f.closed { 66 | return os.ErrInvalid 67 | } 68 | f.committed = true 69 | f.closed = true 70 | var err error 71 | name := f.Name() 72 | defer func() { 73 | if err != nil { 74 | _ = os.Remove(name) //nolint:errcheck // no need to report this error, too 75 | } 76 | }() 77 | if err = f.File.Close(); err != nil { 78 | return err 79 | } 80 | err = os.Rename(name, f.originalName) 81 | return err 82 | } 83 | 84 | // Close the temporary file and remove it, if it hasn't already been committed. If it has been committed, nothing 85 | // happens. 86 | func (f *File) Close() error { 87 | if f.committed { 88 | return nil 89 | } 90 | if f.closed { 91 | return os.ErrInvalid 92 | } 93 | f.closed = true 94 | err := f.File.Close() 95 | if removeErr := os.Remove(f.Name()); removeErr != nil && err == nil { 96 | err = removeErr 97 | } 98 | return err 99 | } 100 | -------------------------------------------------------------------------------- /xio/fs/safe/writefile.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | package safe 11 | 12 | import ( 13 | "bufio" 14 | "io" 15 | "os" 16 | ) 17 | 18 | // WriteFile uses writer to write data safely and atomically to a file. 19 | func WriteFile(filename string, writer func(io.Writer) error) (err error) { 20 | return WriteFileWithMode(filename, writer, 0o644) 21 | } 22 | 23 | // WriteFileWithMode uses writer to write data safely and atomically to a file. 24 | func WriteFileWithMode(filename string, writer func(io.Writer) error, mode os.FileMode) (err error) { 25 | var f *File 26 | f, err = CreateWithMode(filename, mode) 27 | if err != nil { 28 | return 29 | } 30 | w := bufio.NewWriterSize(f, 1<<16) 31 | defer func() { 32 | if closeErr := f.Close(); closeErr != nil && err == nil { 33 | err = closeErr 34 | } 35 | }() 36 | if err = writer(w); err != nil { 37 | return 38 | } 39 | if err = w.Flush(); err != nil { 40 | return 41 | } 42 | if err = f.Commit(); err != nil { 43 | return 44 | } 45 | return 46 | } 47 | -------------------------------------------------------------------------------- /xio/fs/split.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | package fs 11 | 12 | import ( 13 | "path/filepath" 14 | ) 15 | 16 | // Split a path into its component parts. In the case of a full path, the first element will be filepath.Separator, 17 | // possibly prefixed by a volume name. In the case of a relative path, the first element will be ".". 18 | func Split(path string) []string { 19 | var parts []string 20 | path = filepath.Clean(path) 21 | parts = append(parts, filepath.Base(path)) 22 | sep := string(filepath.Separator) 23 | volName := filepath.VolumeName(path) 24 | path = path[len(volName):] 25 | for { 26 | path = filepath.Dir(path) 27 | parts = append(parts, filepath.Base(path)) 28 | if path == "." || path == sep { 29 | break 30 | } 31 | } 32 | result := make([]string, len(parts)) 33 | for i := range parts { 34 | result[len(parts)-(i+1)] = parts[i] 35 | } 36 | if volName != "" && result[0] == sep { 37 | result[0] = volName + sep 38 | } 39 | return result 40 | } 41 | -------------------------------------------------------------------------------- /xio/fs/split_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | package fs_test 11 | 12 | import ( 13 | "path/filepath" 14 | "testing" 15 | 16 | "github.com/richardwilkes/toolbox/check" 17 | "github.com/richardwilkes/toolbox/xio/fs" 18 | ) 19 | 20 | type splitData struct { 21 | in string 22 | out []string 23 | } 24 | 25 | func TestSplit(t *testing.T) { 26 | full := string([]rune{filepath.Separator}) 27 | data := []splitData{ 28 | { 29 | in: "/one/two.txt", 30 | out: []string{full, "one", "two.txt"}, 31 | }, 32 | { 33 | in: "/one", 34 | out: []string{full, "one"}, 35 | }, 36 | { 37 | in: "one", 38 | out: []string{".", "one"}, 39 | }, 40 | { 41 | in: "/one////two.txt", 42 | out: []string{full, "one", "two.txt"}, 43 | }, 44 | { 45 | in: "/one//..//two.txt", 46 | out: []string{full, "two.txt"}, 47 | }, 48 | { 49 | in: "/one/../..//two.txt", 50 | out: []string{full, "two.txt"}, 51 | }, 52 | { 53 | in: "/one/../..//two.txt/", 54 | out: []string{full, "two.txt"}, 55 | }, 56 | { 57 | in: "/one/../..//two.txt/.", 58 | out: []string{full, "two.txt"}, 59 | }, 60 | } 61 | for i, one := range data { 62 | check.Equal(t, one.out, fs.Split(one.in), "%d", i) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /xio/fs/split_windows_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | package fs_test 11 | 12 | import ( 13 | "testing" 14 | 15 | "github.com/richardwilkes/toolbox/check" 16 | "github.com/richardwilkes/toolbox/xio/fs" 17 | ) 18 | 19 | func TestWindowsSplit(t *testing.T) { 20 | data := []splitData{ 21 | { 22 | in: `C:\one/two.txt`, 23 | out: []string{`C:\`, "one", "two.txt"}, 24 | }, 25 | { 26 | in: `\\host\share\one\two.txt`, 27 | out: []string{`\\host\share\`, "one", "two.txt"}, 28 | }, 29 | } 30 | for i, one := range data { 31 | check.Equal(t, one.out, fs.Split(one.in), "%d", i) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /xio/fs/temp.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | package fs 11 | 12 | import ( 13 | "os" 14 | 15 | "github.com/richardwilkes/toolbox/xio/fs/internal" 16 | ) 17 | 18 | // CreateTemp is essentially the same as os.CreateTemp, except it allows you to specify the file mode of the newly 19 | // created file. 20 | func CreateTemp(dir, pattern string, perm os.FileMode) (*os.File, error) { 21 | return internal.CreateTemp(dir, pattern, perm) 22 | } 23 | -------------------------------------------------------------------------------- /xio/fs/walk.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | package fs 11 | 12 | import ( 13 | "errors" 14 | "net/http" 15 | "os" 16 | "path/filepath" 17 | "sort" 18 | 19 | "github.com/richardwilkes/toolbox/xio" 20 | ) 21 | 22 | // Walk performs the same function as filepath.Walk() does, but works on http.FileSystem objects. 23 | func Walk(fs http.FileSystem, root string, walkFn filepath.WalkFunc) error { 24 | info, err := stat(fs, root) 25 | if err != nil { 26 | err = walkFn(root, nil, err) 27 | } else { 28 | err = walk(fs, root, info, walkFn) 29 | } 30 | if errors.Is(err, filepath.SkipDir) { 31 | return nil 32 | } 33 | return err 34 | } 35 | 36 | func stat(fs http.FileSystem, path string) (os.FileInfo, error) { 37 | f, err := fs.Open(path) 38 | if err != nil { 39 | return nil, err 40 | } 41 | var info os.FileInfo 42 | info, err = f.Stat() 43 | xio.CloseIgnoringErrors(f) 44 | return info, err 45 | } 46 | 47 | func walk(fs http.FileSystem, path string, info os.FileInfo, walkFn filepath.WalkFunc) error { 48 | if !info.IsDir() { 49 | return walkFn(path, info, nil) 50 | } 51 | names, err := readDirNames(fs, path) 52 | err1 := walkFn(path, info, err) 53 | if err != nil || err1 != nil { 54 | return err1 55 | } 56 | for _, name := range names { 57 | filename := filepath.Join(path, name) 58 | var fileInfo os.FileInfo 59 | if fileInfo, err = stat(fs, filename); err != nil { 60 | if err = walkFn(filename, fileInfo, err); err != nil && !errors.Is(err, filepath.SkipDir) { 61 | return err 62 | } 63 | } else { 64 | err = walk(fs, filename, fileInfo, walkFn) 65 | if err != nil { 66 | if !fileInfo.IsDir() || !errors.Is(err, filepath.SkipDir) { 67 | return err 68 | } 69 | } 70 | } 71 | } 72 | return nil 73 | } 74 | 75 | func readDirNames(fs http.FileSystem, dirname string) ([]string, error) { 76 | f, err := fs.Open(dirname) 77 | if err != nil { 78 | return nil, err 79 | } 80 | var list []os.FileInfo 81 | list, err = f.Readdir(-1) 82 | xio.CloseIgnoringErrors(f) 83 | if err != nil { 84 | return nil, err 85 | } 86 | names := make([]string, len(list)) 87 | for i := range list { 88 | names[i] = list[i].Name() 89 | } 90 | sort.Strings(names) 91 | return names, nil 92 | } 93 | -------------------------------------------------------------------------------- /xio/fs/yaml.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | package fs 11 | 12 | import ( 13 | "bufio" 14 | "io" 15 | "io/fs" 16 | "os" 17 | 18 | "github.com/richardwilkes/toolbox/errs" 19 | "github.com/richardwilkes/toolbox/xio" 20 | "github.com/richardwilkes/toolbox/xio/fs/safe" 21 | 22 | "gopkg.in/yaml.v3" 23 | ) 24 | 25 | // LoadYAML data from the specified path. 26 | func LoadYAML(path string, data any) error { 27 | f, err := os.Open(path) 28 | if err != nil { 29 | return errs.NewWithCause(path, err) 30 | } 31 | return loadYAML(f, path, data) 32 | } 33 | 34 | // LoadYAMLFromFS data from the specified filesystem path. 35 | func LoadYAMLFromFS(fsys fs.FS, path string, data any) error { 36 | f, err := fsys.Open(path) 37 | if err != nil { 38 | return errs.NewWithCause(path, err) 39 | } 40 | return loadYAML(f, path, data) 41 | } 42 | 43 | func loadYAML(r io.ReadCloser, path string, data any) error { 44 | defer xio.CloseIgnoringErrors(r) 45 | if err := yaml.NewDecoder(bufio.NewReader(r)).Decode(data); err != nil { 46 | return errs.NewWithCause(path, err) 47 | } 48 | return nil 49 | } 50 | 51 | // SaveYAML data to the specified path. 52 | func SaveYAML(path string, data any) error { 53 | return SaveYAMLWithMode(path, data, 0o644) 54 | } 55 | 56 | // SaveYAMLWithMode data to the specified path. 57 | func SaveYAMLWithMode(path string, data any, mode os.FileMode) error { 58 | if err := safe.WriteFileWithMode(path, func(w io.Writer) error { 59 | encoder := yaml.NewEncoder(w) 60 | encoder.SetIndent(2) 61 | if err := encoder.Encode(data); err != nil { 62 | return errs.Wrap(err) 63 | } 64 | return errs.Wrap(encoder.Close()) 65 | }, mode); err != nil { 66 | return errs.NewWithCause(path, err) 67 | } 68 | return nil 69 | } 70 | -------------------------------------------------------------------------------- /xio/fs/yaml_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | package fs_test 11 | 12 | import ( 13 | "os" 14 | "testing" 15 | 16 | "github.com/richardwilkes/toolbox/check" 17 | "github.com/richardwilkes/toolbox/xio/fs" 18 | ) 19 | 20 | func TestLoadSaveYAML(t *testing.T) { 21 | type data struct { 22 | Name string 23 | Count int 24 | } 25 | value := &data{ 26 | Name: "Rich", 27 | Count: 22, 28 | } 29 | f, err := os.CreateTemp("", "yaml_test") 30 | check.NoError(t, err) 31 | check.NoError(t, f.Close()) 32 | check.NoError(t, fs.SaveYAMLWithMode(f.Name(), value, 0o600)) 33 | var value2 data 34 | check.NoError(t, fs.LoadYAML(f.Name(), &value2)) 35 | check.NoError(t, os.Remove(f.Name())) 36 | check.Equal(t, value, &value2) 37 | } 38 | -------------------------------------------------------------------------------- /xio/linewriter.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | package xio 11 | 12 | import ( 13 | "bytes" 14 | ) 15 | 16 | // LineWriter buffers its input into lines before sending each line to an output function without the trailing line 17 | // feed. 18 | type LineWriter struct { 19 | buffer *bytes.Buffer 20 | out func([]byte) 21 | } 22 | 23 | // NewLineWriter creates a new LineWriter. 24 | func NewLineWriter(out func([]byte)) *LineWriter { 25 | return &LineWriter{buffer: &bytes.Buffer{}, out: out} 26 | } 27 | 28 | // Write implements the io.Writer interface. 29 | func (w *LineWriter) Write(data []byte) (n int, err error) { 30 | n = len(data) 31 | for len(data) > 0 { 32 | i := bytes.IndexByte(data, '\n') 33 | if i == -1 { 34 | _, err = w.buffer.Write(data) 35 | return n, err 36 | } 37 | if i > 0 { 38 | if _, err = w.buffer.Write(data[:i]); err != nil { 39 | return n, err 40 | } 41 | } 42 | w.out(w.buffer.Bytes()) 43 | w.buffer.Reset() 44 | data = data[i+1:] 45 | } 46 | return n, nil 47 | } 48 | 49 | // Close implements the io.Closer interface. 50 | func (w *LineWriter) Close() error { 51 | if w.buffer.Len() > 0 { 52 | w.out(w.buffer.Bytes()) 53 | w.buffer.Reset() 54 | } 55 | return nil 56 | } 57 | -------------------------------------------------------------------------------- /xio/network/external_ip.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | package network 11 | 12 | import ( 13 | "io" 14 | "net" 15 | "net/http" 16 | "strings" 17 | "time" 18 | 19 | "github.com/richardwilkes/toolbox/xio" 20 | ) 21 | 22 | var sites = []string{ 23 | // These seem to prefer ipv4 responses, if possible 24 | "http://whatismyip.akamai.com/", 25 | "https://myip.dnsomatic.com/", 26 | "http://api.ipify.org/", 27 | "http://checkip.amazonaws.com/", 28 | 29 | // These seem to prefer ipv6 responses, if possible 30 | "http://icanhazip.com/", 31 | "https://myexternalip.com/raw", 32 | "http://ifconfig.io/ip", 33 | "http://ident.me/", 34 | } 35 | 36 | // ExternalIP returns your IP address as seen by external sites. It does this by iterating through a list of websites 37 | // that will return your IP address as they see it. The first response with a valid IP address will be returned. timeout 38 | // sets the maximum amount of time for each attempt. 39 | func ExternalIP(timeout time.Duration) string { 40 | client := &http.Client{Timeout: timeout} 41 | for _, site := range sites { 42 | if ip := externalIP(client, site); ip != "" { 43 | return ip 44 | } 45 | } 46 | return "" 47 | } 48 | 49 | func externalIP(client *http.Client, site string) string { 50 | if resp, err := client.Get(site); err == nil { //nolint:noctx // The timeout on the client provides the same effect 51 | defer xio.DiscardAndCloseIgnoringErrors(resp.Body) 52 | var body []byte 53 | if body, err = io.ReadAll(resp.Body); err == nil { 54 | if ip := net.ParseIP(strings.TrimSpace(string(body))); ip != nil { 55 | return ip.String() 56 | } 57 | } 58 | } 59 | return "" 60 | } 61 | -------------------------------------------------------------------------------- /xio/network/xhttp/basic_auth.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | // Package xhttp provides HTTP-related utilities. 11 | package xhttp 12 | 13 | import ( 14 | "crypto/subtle" 15 | "fmt" 16 | "net/http" 17 | ) 18 | 19 | // BasicAuth provides basic HTTP authentication. 20 | type BasicAuth struct { 21 | // Lookup provides a way to map a user in a realm to a password. The returned password should have already been 22 | // passed through the Hasher function. 23 | Lookup func(user, realm string) ([]byte, bool) 24 | Hasher func(input string) []byte 25 | Realm string 26 | } 27 | 28 | // Wrap an http.Handler, requiring Basic Authentication. 29 | func (ba *BasicAuth) Wrap(handler http.Handler) http.Handler { 30 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 31 | if user, pw, ok := r.BasicAuth(); ok { 32 | stored, found := ba.Lookup(user, ba.Realm) 33 | passwordMatch := subtle.ConstantTimeCompare(ba.Hasher(pw), stored) == 1 34 | if found && passwordMatch { 35 | if md := MetadataFromRequest(r); md != nil { 36 | md.User = user 37 | md.Logger = md.Logger.With("user", user) 38 | } 39 | handler.ServeHTTP(w, r) 40 | return 41 | } 42 | } 43 | w.Header().Set("WWW-Authenticate", fmt.Sprintf(`Basic realm=%q, charset="UTF-8"`, ba.Realm)) 44 | ErrorStatus(w, http.StatusUnauthorized) 45 | }) 46 | } 47 | -------------------------------------------------------------------------------- /xio/network/xhttp/status_response_writer.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | package xhttp 11 | 12 | import "net/http" 13 | 14 | // StatusResponseWriter wraps an http.ResponseWriter and provides methods to retrieve the status code and number of 15 | // bytes written. 16 | type StatusResponseWriter struct { 17 | Original http.ResponseWriter 18 | Head bool 19 | status int 20 | written int 21 | } 22 | 23 | // Status returns the status that was set, or http.StatusOK if no call to WriteHeader() was made. 24 | func (w *StatusResponseWriter) Status() int { 25 | if w.status != 0 { 26 | return w.status 27 | } 28 | return http.StatusOK 29 | } 30 | 31 | // BytesWritten returns the number of bytes written. 32 | func (w *StatusResponseWriter) BytesWritten() int { 33 | return w.written 34 | } 35 | 36 | // Header implements http.ResponseWriter. 37 | func (w *StatusResponseWriter) Header() http.Header { 38 | return w.Original.Header() 39 | } 40 | 41 | // Write implements http.ResponseWriter. 42 | func (w *StatusResponseWriter) Write(data []byte) (int, error) { 43 | if w.Head { 44 | return len(data), nil 45 | } 46 | n, err := w.Original.Write(data) 47 | w.written += n 48 | return n, err 49 | } 50 | 51 | // WriteHeader implements http.ResponseWriter. 52 | func (w *StatusResponseWriter) WriteHeader(status int) { 53 | w.status = status 54 | w.Original.WriteHeader(status) 55 | } 56 | 57 | // Flush implements http.Flusher. 58 | func (w *StatusResponseWriter) Flush() { 59 | f, ok := w.Original.(http.Flusher) 60 | if ok { 61 | f.Flush() 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /xio/network/xhttp/utility.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | package xhttp 11 | 12 | import "net/http" 13 | 14 | // ErrorStatus sends an HTTP response header with 'statusCode' and follows it with the standard text for that code as 15 | // the body. 16 | func ErrorStatus(w http.ResponseWriter, statusCode int) { 17 | http.Error(w, http.StatusText(statusCode), statusCode) 18 | } 19 | 20 | // DisableCaching disables caching for the given response writer. To be effective, should be called before any data is 21 | // written. 22 | func DisableCaching(w http.ResponseWriter) { 23 | header := w.Header() 24 | header.Set("Cache-Control", "no-store") 25 | header.Set("Pragma", "no-cache") 26 | } 27 | -------------------------------------------------------------------------------- /xio/retrieve.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | package xio 11 | 12 | import ( 13 | "context" 14 | "io" 15 | "net/http" 16 | "net/url" 17 | "os" 18 | "strings" 19 | 20 | "github.com/richardwilkes/toolbox/errs" 21 | ) 22 | 23 | // RetrieveData loads the bytes from the given file path or URL of type file, http, or https. 24 | func RetrieveData(filePathOrURL string) ([]byte, error) { 25 | return RetrieveDataWithContext(context.Background(), filePathOrURL) 26 | } 27 | 28 | // RetrieveDataWithContext loads the bytes from the given file path or URL of type file, http, or https. 29 | func RetrieveDataWithContext(ctx context.Context, filePathOrURL string) ([]byte, error) { 30 | if strings.HasPrefix(filePathOrURL, "http://") || 31 | strings.HasPrefix(filePathOrURL, "https://") || 32 | strings.HasPrefix(filePathOrURL, "file://") { 33 | return RetrieveDataFromURLWithContext(ctx, filePathOrURL) 34 | } 35 | data, err := os.ReadFile(filePathOrURL) 36 | if err != nil { 37 | return nil, errs.NewWithCause(filePathOrURL, err) 38 | } 39 | return data, nil 40 | } 41 | 42 | // RetrieveDataFromURL loads the bytes from the given URL of type file, http, or https. 43 | func RetrieveDataFromURL(urlStr string) ([]byte, error) { 44 | return RetrieveDataFromURLWithContext(context.Background(), urlStr) 45 | } 46 | 47 | // RetrieveDataFromURLWithContext loads the bytes from the given URL of type file, http, or https. 48 | func RetrieveDataFromURLWithContext(ctx context.Context, urlStr string) ([]byte, error) { 49 | u, err := url.Parse(urlStr) 50 | if err != nil { 51 | return nil, errs.NewWithCause(urlStr, err) 52 | } 53 | var data []byte 54 | switch u.Scheme { 55 | case "file": 56 | if data, err = os.ReadFile(u.Path); err != nil { 57 | return nil, errs.NewWithCause(urlStr, err) 58 | } 59 | return data, nil 60 | case "http", "https": 61 | var req *http.Request 62 | req, err = http.NewRequestWithContext(ctx, http.MethodGet, urlStr, http.NoBody) 63 | if err != nil { 64 | return nil, errs.NewWithCause("unable to create request", err) 65 | } 66 | var rsp *http.Response 67 | if rsp, err = http.DefaultClient.Do(req); err != nil { 68 | return nil, errs.NewWithCause(urlStr, err) 69 | } 70 | defer DiscardAndCloseIgnoringErrors(rsp.Body) 71 | if rsp.StatusCode < 200 || rsp.StatusCode > 299 { 72 | return nil, errs.NewWithCause(urlStr, errs.Newf("received status %d (%s)", rsp.StatusCode, rsp.Status)) 73 | } 74 | data, err = io.ReadAll(rsp.Body) 75 | if err != nil { 76 | return nil, errs.NewWithCause(urlStr, err) 77 | } 78 | return data, nil 79 | default: 80 | return nil, errs.Newf("invalid url: %s", urlStr) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /xio/tee.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | package xio 11 | 12 | import "io" 13 | 14 | // TeeWriter is a writer that writes to multiple other writers. 15 | type TeeWriter struct { 16 | Writers []io.Writer 17 | } 18 | 19 | // Write to each of the underlying streams. 20 | func (t *TeeWriter) Write(p []byte) (n int, err error) { 21 | var curErr error 22 | for _, w := range t.Writers { 23 | if n, curErr = w.Write(p); curErr != nil { 24 | err = curErr 25 | } 26 | } 27 | return 28 | } 29 | -------------------------------------------------------------------------------- /xio/term/getch.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | //go:build !windows 11 | 12 | package term 13 | 14 | import ( 15 | "github.com/pkg/term" 16 | ) 17 | 18 | // Read a byte from the terminal. 19 | func Read() (ch byte, ok bool) { 20 | t, err := term.Open("/dev/tty") 21 | if err != nil { 22 | return 0, false 23 | } 24 | err = term.RawMode(t) 25 | if err != nil { 26 | return 0, false 27 | } 28 | bytes := make([]byte, 1) 29 | numRead, err := t.Read(bytes) 30 | if altErr := t.Restore(); altErr != nil && err == nil { 31 | err = altErr 32 | } 33 | if altErr := t.Close(); altErr != nil && err == nil { 34 | err = altErr 35 | } 36 | if err != nil || numRead == 0 { 37 | return 0, false 38 | } 39 | return bytes[0], true 40 | } 41 | -------------------------------------------------------------------------------- /xio/term/size_other.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | //go:build !darwin && !dragonfly && !freebsd && !linux && !netbsd && !openbsd && !solaris 11 | 12 | package term 13 | 14 | // Size returns the number of columns and rows comprising the terminal. 15 | func Size() (columns, rows int) { 16 | return defColumns, defRows 17 | } 18 | -------------------------------------------------------------------------------- /xio/term/size_unix.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | //go:build darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris 11 | 12 | package term 13 | 14 | import ( 15 | "syscall" 16 | "unsafe" 17 | ) 18 | 19 | // Size returns the number of columns and rows comprising the terminal. 20 | func Size() (columns, rows int) { 21 | var ws [4]uint16 22 | if _, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(0), syscall.TIOCGWINSZ, uintptr(unsafe.Pointer(&ws[0]))); errno == 0 { 23 | return int(ws[1]), int(ws[0]) 24 | } 25 | return defColumns, defRows 26 | } 27 | -------------------------------------------------------------------------------- /xio/term/terminal.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | // Package term provides terminal utilities. 11 | package term 12 | 13 | import ( 14 | "fmt" 15 | "io" 16 | "strings" 17 | ) 18 | 19 | const ( 20 | defColumns = 80 21 | defRows = 24 22 | ) 23 | 24 | // WrapText prints the 'prefix' to 'out' and then wraps 'text' in the remaining space. 25 | func WrapText(out io.Writer, prefix, text string) { 26 | fmt.Fprint(out, prefix) 27 | avail, _ := Size() 28 | avail -= 1 + len(prefix) 29 | if avail < 1 { 30 | avail = 1 31 | } 32 | remaining := avail 33 | indent := strings.Repeat(" ", len(prefix)) 34 | for _, line := range strings.Split(text, "\n") { 35 | for _, ch := range line { 36 | if ch == ' ' { 37 | fmt.Fprint(out, " ") 38 | remaining-- 39 | } else { 40 | break 41 | } 42 | } 43 | for i, token := range strings.Fields(line) { 44 | length := len(token) + 1 45 | if i != 0 { 46 | if length > remaining { 47 | fmt.Fprintln(out) 48 | fmt.Fprint(out, indent) 49 | remaining = avail 50 | } else { 51 | fmt.Fprint(out, " ") 52 | } 53 | } 54 | fmt.Fprint(out, token) 55 | remaining -= length 56 | } 57 | fmt.Fprintln(out) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /xio/term/terminal_bsd.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | //go:build darwin || freebsd || openbsd || netbsd || dragonfly 11 | 12 | package term 13 | 14 | import "syscall" 15 | 16 | const ioctlReadTermios = syscall.TIOCGETA 17 | -------------------------------------------------------------------------------- /xio/term/terminal_linux.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | package term 11 | 12 | import "syscall" 13 | 14 | const ioctlReadTermios = syscall.TCGETS 15 | -------------------------------------------------------------------------------- /xio/term/terminal_other.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | //go:build !darwin && !dragonfly && !freebsd && !linux && !netbsd && !openbsd 11 | 12 | package term 13 | 14 | import ( 15 | "io" 16 | ) 17 | 18 | // IsTerminal returns true if the writer's file descriptor is a terminal. 19 | func IsTerminal(_ io.Writer) bool { 20 | return false 21 | } 22 | -------------------------------------------------------------------------------- /xio/term/terminal_unix.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | //go:build darwin || dragonfly || freebsd || linux || netbsd || openbsd 11 | 12 | package term 13 | 14 | import ( 15 | "io" 16 | "os" 17 | "syscall" 18 | "unsafe" 19 | ) 20 | 21 | // IsTerminal returns true if the writer's file descriptor is a terminal. 22 | func IsTerminal(f io.Writer) bool { 23 | var termios syscall.Termios 24 | switch v := f.(type) { 25 | case *os.File: 26 | _, _, errno := syscall.Syscall6(syscall.SYS_IOCTL, v.Fd(), ioctlReadTermios, uintptr(unsafe.Pointer(&termios)), 0, 0, 0) 27 | return errno == 0 28 | default: 29 | return false 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /xmath/bitset_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | package xmath 11 | 12 | import ( 13 | "testing" 14 | 15 | "github.com/richardwilkes/toolbox/check" 16 | ) 17 | 18 | func TestBitSet(t *testing.T) { 19 | var bs BitSet 20 | check.Equal(t, 0, bs.Count()) 21 | bs.Set(0) 22 | check.Equal(t, 1, bs.Count()) 23 | bs.Set(7) 24 | check.Equal(t, 2, bs.Count()) 25 | bs.Set(dataBitsPerWord - 1) 26 | check.Equal(t, 3, bs.Count()) 27 | bs.Set(dataBitsPerWord) 28 | check.Equal(t, 4, bs.Count()) 29 | bs.Set(dataBitsPerWord + 1) 30 | check.Equal(t, 5, bs.Count()) 31 | bs.Set(0) 32 | check.Equal(t, 5, bs.Count()) 33 | bs.Clear(0) 34 | check.Equal(t, 4, bs.Count()) 35 | bs.Clear(1) 36 | check.Equal(t, 4, bs.Count()) 37 | bs.Clear(1000) 38 | check.Equal(t, 4, bs.Count()) 39 | check.False(t, bs.State(0)) 40 | check.False(t, bs.State(1)) 41 | check.True(t, bs.State(7)) 42 | check.False(t, bs.State(77)) 43 | check.True(t, bs.State(dataBitsPerWord)) 44 | bs.Flip(22) 45 | check.True(t, bs.State(22)) 46 | bs.Flip(22) 47 | check.False(t, bs.State(22)) 48 | check.Equal(t, 7, bs.NextSet(0)) 49 | check.Equal(t, 7, bs.NextSet(7)) 50 | check.Equal(t, dataBitsPerWord-1, bs.NextSet(8)) 51 | check.Equal(t, dataBitsPerWord, bs.NextSet(dataBitsPerWord)) 52 | bs.Set(1234) 53 | check.Equal(t, 1234, bs.NextSet(dataBitsPerWord+2)) 54 | check.Equal(t, 0, bs.NextClear(0)) 55 | check.Equal(t, dataBitsPerWord+2, bs.NextClear(dataBitsPerWord-1)) 56 | check.Equal(t, 1235, bs.NextClear(1234)) 57 | bs.Set(dataBitsPerWord*100 - 1) 58 | check.Equal(t, dataBitsPerWord*100, bs.NextClear(dataBitsPerWord*100-1)) 59 | check.Equal(t, dataBitsPerWord*100-1, bs.PreviousSet(dataBitsPerWord*100)) 60 | check.Equal(t, 1234, bs.PreviousSet(dataBitsPerWord*100-2)) 61 | check.Equal(t, -1, bs.PreviousSet(0)) 62 | check.Equal(t, dataBitsPerWord*1000, bs.PreviousClear(dataBitsPerWord*1000)) 63 | check.Equal(t, dataBitsPerWord*100-2, bs.PreviousClear(dataBitsPerWord*100-1)) 64 | check.Equal(t, 0, bs.PreviousClear(0)) 65 | bs.Set(0) 66 | check.Equal(t, -1, bs.PreviousClear(0)) 67 | 68 | bs.Reset() 69 | bs.Set(65) 70 | bs.SetRange(10, 300) 71 | check.Equal(t, 291, bs.Count()) 72 | for i := 10; i < 301; i++ { 73 | check.True(t, bs.State(i)) 74 | } 75 | check.Equal(t, 301, bs.NextClear(10)) 76 | check.Equal(t, 9, bs.PreviousClear(300)) 77 | check.Equal(t, 10, bs.NextSet(0)) 78 | check.Equal(t, 300, bs.PreviousSet(1000)) 79 | bs.ClearRange(15, 295) 80 | check.Equal(t, 10, bs.Count()) 81 | for i := 15; i < 296; i++ { 82 | check.False(t, bs.State(i)) 83 | } 84 | bs.FlipRange(10, 300) 85 | check.Equal(t, 281, bs.Count()) 86 | } 87 | -------------------------------------------------------------------------------- /xmath/constants.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | // Package xmath provides math-related utilities. 11 | package xmath 12 | 13 | import ( 14 | "math" 15 | ) 16 | 17 | // Mathematical constants. Mostly just re-exported from the math package for convenience. 18 | const ( 19 | E = math.E 20 | Pi = math.Pi 21 | Phi = math.Phi 22 | 23 | Sqrt2 = math.Sqrt2 24 | SqrtE = math.SqrtE 25 | SqrtPi = math.SqrtPi 26 | SqrtPhi = math.SqrtPhi 27 | 28 | Ln2 = math.Ln2 29 | Log2E = 1 / Ln2 30 | Ln10 = math.Ln10 31 | Log10E = 1 / Ln10 32 | ) 33 | 34 | // Floating-point limit values. Mostly just re-exported from the math package for convenience. Max is the largest finite 35 | // value representable by the type. SmallestNonzero is the smallest positive, non-zero value representable by the type. 36 | const ( 37 | MaxFloat32 = math.MaxFloat32 38 | SmallestNonzeroFloat32 = math.SmallestNonzeroFloat32 39 | MaxFloat64 = math.MaxFloat64 40 | SmallestNonzeroFloat64 = math.SmallestNonzeroFloat64 41 | ) 42 | 43 | // Integer limit values. Mostly just re-exported from the math package for convenience. 44 | const ( 45 | MaxInt = math.MaxInt 46 | MinInt = math.MinInt 47 | MaxInt8 = math.MaxInt8 48 | MinInt8 = math.MinInt8 49 | MaxInt16 = math.MaxInt16 50 | MinInt16 = math.MinInt16 51 | MaxInt32 = math.MaxInt32 52 | MinInt32 = math.MinInt32 53 | MaxInt64 = math.MaxInt64 54 | MinInt64 = math.MinInt64 55 | MaxUint = math.MaxUint 56 | MaxUint8 = math.MaxUint8 57 | MaxUint16 = math.MaxUint16 58 | MaxUint32 = math.MaxUint32 59 | MaxUint64 = math.MaxUint64 60 | ) 61 | 62 | const ( 63 | // DegreesToRadians converts a value in degrees to radians when multiplied with the value. 64 | DegreesToRadians = math.Pi / 180 65 | // RadiansToDegrees converts a value in radians to degrees when multiplied with the value. 66 | RadiansToDegrees = 180 / math.Pi 67 | ) 68 | -------------------------------------------------------------------------------- /xmath/crc/crc.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | package crc 11 | 12 | import ( 13 | "hash/crc64" 14 | ) 15 | 16 | var crcTable = crc64.MakeTable(crc64.ECMA) 17 | 18 | // Bool returns the CRC-64 value for the given data starting with the given crc value. 19 | func Bool(crc uint64, b bool) uint64 { 20 | var data [1]byte 21 | if b { 22 | data[0] = 1 23 | } 24 | return crc64.Update(crc, crcTable, data[:]) 25 | } 26 | 27 | // Bytes returns the CRC-64 value for the given data starting with the given crc value. 28 | func Bytes(crc uint64, data []byte) uint64 { 29 | return crc64.Update(crc, crcTable, data) 30 | } 31 | 32 | // String returns the CRC-64 value for the given data starting with the given crc value. 33 | func String(crc uint64, data string) uint64 { 34 | return crc64.Update(crc, crcTable, []byte(data)) 35 | } 36 | 37 | // Byte returns the CRC-64 value for the given data starting with the given crc value. 38 | func Byte(crc uint64, data byte) uint64 { 39 | var buffer [1]byte 40 | buffer[0] = data 41 | return crc64.Update(crc, crcTable, buffer[:]) 42 | } 43 | 44 | // Number returns the CRC-64 value for the given data starting with the given crc value. 45 | func Number[T ~int64 | ~uint64 | ~int | ~uint](crc uint64, data T) uint64 { 46 | var buffer [8]byte 47 | d := uint64(data) 48 | buffer[0] = byte(d) 49 | buffer[1] = byte(d >> 8) 50 | buffer[2] = byte(d >> 16) 51 | buffer[3] = byte(d >> 24) 52 | buffer[4] = byte(d >> 32) 53 | buffer[5] = byte(d >> 40) 54 | buffer[6] = byte(d >> 48) 55 | buffer[7] = byte(d >> 56) 56 | return crc64.Update(crc, crcTable, buffer[:]) 57 | } 58 | -------------------------------------------------------------------------------- /xmath/fixed/errors.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | package fixed 11 | 12 | import "errors" 13 | 14 | // ErrDoesNotFitInRequestedType is returned from the fixed-point CheckedAs() functions if the requested type cannot 15 | // exactly represent the value. 16 | var ErrDoesNotFitInRequestedType = errors.New("does not fit in requested type") 17 | -------------------------------------------------------------------------------- /xmath/fixed/f128/fraction.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | package f128 11 | 12 | import ( 13 | "encoding/json" 14 | "strings" 15 | 16 | "github.com/richardwilkes/toolbox/xmath/fixed" 17 | ) 18 | 19 | // Fraction holds a fractional value. 20 | type Fraction[T fixed.Dx] struct { 21 | Numerator Int[T] 22 | Denominator Int[T] 23 | } 24 | 25 | // NewFraction creates a new fractional value from a string. 26 | func NewFraction[T fixed.Dx](s string) Fraction[T] { 27 | parts := strings.SplitN(s, "/", 2) 28 | f := Fraction[T]{ 29 | Numerator: FromStringForced[T](strings.TrimSpace(parts[0])), 30 | Denominator: From[T, int](1), 31 | } 32 | if len(parts) > 1 { 33 | f.Denominator = FromStringForced[T](strings.TrimSpace(parts[1])) 34 | } 35 | return f 36 | } 37 | 38 | // Normalize the fraction, eliminating any division by zero and ensuring a positive denominator. 39 | func (f *Fraction[T]) Normalize() { 40 | var zero Int[T] 41 | if f.Denominator == zero { 42 | f.Numerator = Int[T]{} 43 | f.Denominator = From[T, int](1) 44 | } else if f.Denominator.LessThan(zero) { 45 | negOne := From[T, int](-1) 46 | f.Numerator = f.Numerator.Mul(negOne) 47 | f.Denominator = f.Denominator.Mul(negOne) 48 | } 49 | } 50 | 51 | // Value returns the computed value. 52 | func (f Fraction[T]) Value() Int[T] { 53 | n := f 54 | n.Normalize() 55 | return n.Numerator.Div(n.Denominator) 56 | } 57 | 58 | // StringWithSign returns the same as String(), but prefixes the value with a '+' if it is positive. 59 | func (f Fraction[T]) StringWithSign() string { 60 | n := f 61 | n.Normalize() 62 | s := n.Numerator.StringWithSign() 63 | if n.Denominator == From[T, int](1) { 64 | return s 65 | } 66 | return s + "/" + n.Denominator.String() 67 | } 68 | 69 | func (f Fraction[T]) String() string { 70 | n := f 71 | n.Normalize() 72 | s := n.Numerator.String() 73 | if n.Denominator == From[T, int](1) { 74 | return s 75 | } 76 | return s + "/" + n.Denominator.String() 77 | } 78 | 79 | // MarshalJSON implements json.Marshaler. 80 | func (f Fraction[T]) MarshalJSON() ([]byte, error) { 81 | return json.Marshal(f.String()) 82 | } 83 | 84 | // UnmarshalJSON implements json.Unmarshaler. 85 | func (f *Fraction[T]) UnmarshalJSON(in []byte) error { 86 | var s string 87 | if err := json.Unmarshal(in, &s); err != nil { 88 | return err 89 | } 90 | *f = NewFraction[T](s) 91 | f.Normalize() 92 | return nil 93 | } 94 | -------------------------------------------------------------------------------- /xmath/fixed/f128/fraction_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | package f128_test 11 | 12 | import ( 13 | "testing" 14 | 15 | "github.com/richardwilkes/toolbox/check" 16 | "github.com/richardwilkes/toolbox/xmath/fixed" 17 | "github.com/richardwilkes/toolbox/xmath/fixed/f128" 18 | ) 19 | 20 | func TestFraction(t *testing.T) { 21 | check.Equal(t, f128.FromStringForced[fixed.D4]("0.3333"), f128.NewFraction[fixed.D4]("1/3").Value()) 22 | check.Equal(t, f128.FromStringForced[fixed.D4]("0.3333"), f128.NewFraction[fixed.D4]("1 / 3").Value()) 23 | check.Equal(t, f128.FromStringForced[fixed.D4]("0.3333"), f128.NewFraction[fixed.D4]("-1/-3").Value()) 24 | check.Equal(t, f128.From[fixed.D4, int](0), f128.NewFraction[fixed.D4]("5/0").Value()) 25 | check.Equal(t, f128.From[fixed.D4, int](5), f128.NewFraction[fixed.D4]("5/1").Value()) 26 | check.Equal(t, f128.From[fixed.D4, int](-5), f128.NewFraction[fixed.D4]("-5/1").Value()) 27 | check.Equal(t, f128.From[fixed.D4, int](-5), f128.NewFraction[fixed.D4]("5/-1").Value()) 28 | } 29 | -------------------------------------------------------------------------------- /xmath/fixed/f64/fraction.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | package f64 11 | 12 | import ( 13 | "encoding/json" 14 | "strings" 15 | 16 | "github.com/richardwilkes/toolbox/xmath/fixed" 17 | ) 18 | 19 | // Fraction holds a fractional value. 20 | type Fraction[T fixed.Dx] struct { 21 | Numerator Int[T] 22 | Denominator Int[T] 23 | } 24 | 25 | // NewFraction creates a new fractional value from a string. 26 | func NewFraction[T fixed.Dx](s string) Fraction[T] { 27 | parts := strings.SplitN(s, "/", 2) 28 | f := Fraction[T]{ 29 | Numerator: FromStringForced[T](strings.TrimSpace(parts[0])), 30 | Denominator: From[T, int](1), 31 | } 32 | if len(parts) > 1 { 33 | f.Denominator = FromStringForced[T](strings.TrimSpace(parts[1])) 34 | } 35 | return f 36 | } 37 | 38 | // Normalize the fraction, eliminating any division by zero and ensuring a positive denominator. 39 | func (f *Fraction[T]) Normalize() { 40 | if f.Denominator == 0 { 41 | f.Numerator = 0 42 | f.Denominator = From[T, int](1) 43 | } else if f.Denominator < 0 { 44 | negOne := From[T, int](-1) 45 | f.Numerator = f.Numerator.Mul(negOne) 46 | f.Denominator = f.Denominator.Mul(negOne) 47 | } 48 | } 49 | 50 | // Value returns the computed value. 51 | func (f Fraction[T]) Value() Int[T] { 52 | n := f 53 | n.Normalize() 54 | return n.Numerator.Div(n.Denominator) 55 | } 56 | 57 | // StringWithSign returns the same as String(), but prefixes the value with a '+' if it is positive. 58 | func (f Fraction[T]) StringWithSign() string { 59 | n := f 60 | n.Normalize() 61 | s := n.Numerator.StringWithSign() 62 | if n.Denominator == From[T, int](1) { 63 | return s 64 | } 65 | return s + "/" + n.Denominator.String() 66 | } 67 | 68 | func (f Fraction[T]) String() string { 69 | n := f 70 | n.Normalize() 71 | s := n.Numerator.String() 72 | if n.Denominator == From[T, int](1) { 73 | return s 74 | } 75 | return s + "/" + n.Denominator.String() 76 | } 77 | 78 | // MarshalJSON implements json.Marshaler. 79 | func (f Fraction[T]) MarshalJSON() ([]byte, error) { 80 | return json.Marshal(f.String()) 81 | } 82 | 83 | // UnmarshalJSON implements json.Unmarshaler. 84 | func (f *Fraction[T]) UnmarshalJSON(in []byte) error { 85 | var s string 86 | if err := json.Unmarshal(in, &s); err != nil { 87 | return err 88 | } 89 | *f = NewFraction[T](s) 90 | f.Normalize() 91 | return nil 92 | } 93 | -------------------------------------------------------------------------------- /xmath/fixed/f64/fraction_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | package f64_test 11 | 12 | import ( 13 | "testing" 14 | 15 | "github.com/richardwilkes/toolbox/check" 16 | "github.com/richardwilkes/toolbox/xmath/fixed" 17 | "github.com/richardwilkes/toolbox/xmath/fixed/f64" 18 | ) 19 | 20 | func TestFraction(t *testing.T) { 21 | check.Equal(t, f64.FromStringForced[fixed.D4]("0.3333"), f64.NewFraction[fixed.D4]("1/3").Value()) 22 | check.Equal(t, f64.FromStringForced[fixed.D4]("0.3333"), f64.NewFraction[fixed.D4]("1 / 3").Value()) 23 | check.Equal(t, f64.FromStringForced[fixed.D4]("0.3333"), f64.NewFraction[fixed.D4]("-1/-3").Value()) 24 | check.Equal(t, f64.From[fixed.D4, int](0), f64.NewFraction[fixed.D4]("5/0").Value()) 25 | check.Equal(t, f64.From[fixed.D4, int](5), f64.NewFraction[fixed.D4]("5/1").Value()) 26 | check.Equal(t, f64.From[fixed.D4, int](-5), f64.NewFraction[fixed.D4]("-5/1").Value()) 27 | check.Equal(t, f64.From[fixed.D4, int](-5), f64.NewFraction[fixed.D4]("5/-1").Value()) 28 | } 29 | -------------------------------------------------------------------------------- /xmath/geom/poly/README.md: -------------------------------------------------------------------------------- 1 | # Warning! 2 | 3 | The polygon boolean operation code doesn't function as expected in all cases. It does work for most 4 | cases, but I recommend against using it in production code at the moment. -------------------------------------------------------------------------------- /xmath/geom/poly/contour.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | package poly 11 | 12 | import ( 13 | "strings" 14 | 15 | "github.com/richardwilkes/toolbox/xmath" 16 | "github.com/richardwilkes/toolbox/xmath/geom" 17 | "golang.org/x/exp/constraints" 18 | ) 19 | 20 | // Contour is a sequence of vertices connected by line segments, forming a closed shape. 21 | type Contour[T constraints.Float] []geom.Point[T] 22 | 23 | // Clone returns a copy of this contour. 24 | func (c Contour[T]) Clone() Contour[T] { 25 | if len(c) == 0 { 26 | return nil 27 | } 28 | clone := make(Contour[T], len(c)) 29 | copy(clone, c) 30 | return clone 31 | } 32 | 33 | // Bounds returns the bounding rectangle of the contour. 34 | func (c Contour[T]) Bounds() geom.Rect[T] { 35 | if len(c) == 0 { 36 | return geom.Rect[T]{} 37 | } 38 | minX := xmath.MaxValue[T]() 39 | minY := minX 40 | maxX := xmath.MinValue[T]() 41 | maxY := maxX 42 | for _, p := range c { 43 | if p.X > maxX { 44 | maxX = p.X 45 | } 46 | if p.X < minX { 47 | minX = p.X 48 | } 49 | if p.Y > maxY { 50 | maxY = p.Y 51 | } 52 | if p.Y < minY { 53 | minY = p.Y 54 | } 55 | } 56 | return geom.NewRect(minX, minY, 1+maxX-minX, 1+maxY-minY) 57 | } 58 | 59 | // Contains returns true if the point is contained by the contour. 60 | func (c Contour[T]) Contains(pt geom.Point[T]) bool { 61 | var count int 62 | for i := range c { 63 | cur := c[i] 64 | bottom := cur 65 | next := c[(i+1)%len(c)] 66 | top := next 67 | if bottom.Y > top.Y { 68 | bottom, top = top, bottom 69 | } 70 | if pt.Y >= bottom.Y && pt.Y < top.Y && pt.X < max(cur.X, next.X) && next.Y != cur.Y && 71 | (cur.X == next.X || pt.X <= (pt.Y-cur.Y)*(next.X-cur.X)/(next.Y-cur.Y)+cur.X) { 72 | count++ 73 | } 74 | } 75 | return count%2 == 1 76 | } 77 | 78 | func (c Contour[T]) String() string { 79 | var buffer strings.Builder 80 | buffer.WriteByte('{') 81 | for j, pt := range c { 82 | if j != 0 { 83 | buffer.WriteByte(',') 84 | } 85 | buffer.WriteByte('{') 86 | buffer.WriteString(pt.String()) 87 | buffer.WriteByte('}') 88 | } 89 | buffer.WriteByte('}') 90 | return buffer.String() 91 | } 92 | -------------------------------------------------------------------------------- /xmath/geom/poly/scan_beam_tree.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | package poly 11 | 12 | import "golang.org/x/exp/constraints" 13 | 14 | type scanBeamTree[T constraints.Float] struct { 15 | root *scanBeamNode[T] 16 | entries int 17 | } 18 | 19 | type scanBeamNode[T constraints.Float] struct { 20 | y T 21 | less *scanBeamNode[T] 22 | more *scanBeamNode[T] 23 | } 24 | 25 | func (s *scanBeamTree[T]) add(y T) { 26 | s.addToScanBeamTreeAt(&s.root, y) 27 | } 28 | 29 | func (s *scanBeamTree[T]) addToScanBeamTreeAt(node **scanBeamNode[T], y T) { 30 | switch { 31 | case *node == nil: 32 | *node = &scanBeamNode[T]{y: y} 33 | s.entries++ 34 | case (*node).y > y: 35 | s.addToScanBeamTreeAt(&(*node).less, y) 36 | case (*node).y < y: 37 | s.addToScanBeamTreeAt(&(*node).more, y) 38 | default: 39 | } 40 | } 41 | 42 | func (s *scanBeamTree[T]) buildScanBeamTable() []T { 43 | table := make([]T, s.entries) 44 | if s.root != nil { 45 | s.root.buildScanBeamTableEntries(0, table) 46 | } 47 | return table 48 | } 49 | 50 | func (sbt *scanBeamNode[T]) buildScanBeamTableEntries(index int, table []T) int { 51 | if sbt.less != nil { 52 | index = sbt.less.buildScanBeamTableEntries(index, table) 53 | } 54 | table[index] = sbt.y 55 | index++ 56 | if sbt.more != nil { 57 | index = sbt.more.buildScanBeamTableEntries(index, table) 58 | } 59 | return index 60 | } 61 | -------------------------------------------------------------------------------- /xmath/geom/poly/shapes.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | package poly 11 | 12 | import ( 13 | "math" 14 | 15 | "github.com/richardwilkes/toolbox/xmath" 16 | "github.com/richardwilkes/toolbox/xmath/geom" 17 | "golang.org/x/exp/constraints" 18 | ) 19 | 20 | // FromRect returns a Polygon in the shape of the specified rectangle. 21 | func FromRect[T constraints.Float](r geom.Rect[T]) Polygon[T] { 22 | right := r.Right() - 1 23 | bottom := r.Bottom() - 1 24 | return Polygon[T]{Contour[T]{r.Point, geom.NewPoint(r.X, bottom), geom.NewPoint(right, bottom), geom.NewPoint(right, r.Y)}} 25 | } 26 | 27 | // FromEllipse returns a Polygon that approximates an ellipse filling the given Rect. 'sections' indicates how many 28 | // segments to break the ellipse contour into. Passing a value less than 4 for 'sections' will result in an automatic 29 | // choice based on a call to EllipseSegmentCount, using half of the longest dimension for the 'r' parameter and 0.2 for 30 | // the 'e' parameter. 31 | func FromEllipse[T constraints.Float](r geom.Rect[T], sections int) Polygon[T] { 32 | if sections < 4 { 33 | sections = EllipseSegmentCount(max(r.Width, r.Height)/2, 0.2) 34 | } 35 | halfWidth := r.Width / 2 36 | halfHeight := r.Height / 2 37 | inc := math.Pi * 2 / T(sections) 38 | center := r.Center() 39 | contour := make(Contour[T], sections) 40 | var angle T 41 | for i := range sections { 42 | contour[i] = geom.NewPoint(center.X+xmath.Cos(angle)*halfWidth, center.Y+xmath.Sin(angle)*halfHeight) 43 | angle += inc 44 | } 45 | return Polygon[T]{contour} 46 | } 47 | 48 | // EllipseSegmentCount returns a suggested number of segments to use when generating an ellipse. 'r' is the largest 49 | // radius of the ellipse. 'e' is the acceptable error, typically 1 or less. 50 | func EllipseSegmentCount[T constraints.Float](r, e T) int { 51 | d := 1 - e/r 52 | return max(int(xmath.Ceil(2*math.Pi/xmath.Acos(2*d*d-1))), 4) 53 | } 54 | -------------------------------------------------------------------------------- /xmath/geom/poly/vertex_type.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | package poly 11 | 12 | type vertexType int 13 | 14 | const ( 15 | _ vertexType = iota 16 | externalMaximum 17 | externalLeftIntermediate 18 | _ // topEdge, not used 19 | externalRightIntermediate 20 | rightEdge 21 | internalMaximumAndMinimum 22 | internalMinimum 23 | externalMinimum 24 | externalMaximumAndMinimum 25 | leftEdge 26 | internalLeftIntermediate 27 | _ // bottomEdge, not used 28 | internalRightIntermediate 29 | internalMaximum 30 | _ // non-intersection, not used 31 | ) 32 | 33 | func calcVertexType(tr, tl, br, bl bool) vertexType { 34 | var vt vertexType 35 | if tr { 36 | vt = externalMaximum 37 | } 38 | if tl { 39 | vt |= externalLeftIntermediate 40 | } 41 | if br { 42 | vt |= externalRightIntermediate 43 | } 44 | if bl { 45 | vt |= externalMinimum 46 | } 47 | return vt 48 | } 49 | -------------------------------------------------------------------------------- /xmath/geom/size.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | package geom 11 | 12 | import ( 13 | "fmt" 14 | 15 | "github.com/richardwilkes/toolbox/xmath" 16 | ) 17 | 18 | // Size defines a width and height. 19 | type Size[T xmath.Numeric] struct { 20 | Width T `json:"w"` 21 | Height T `json:"h"` 22 | } 23 | 24 | // NewSize creates a new Size. 25 | func NewSize[T xmath.Numeric](width, height T) Size[T] { 26 | return Size[T]{ 27 | Width: width, 28 | Height: height, 29 | } 30 | } 31 | 32 | // ConvertSize converts a Size of type F into one of type T. 33 | func ConvertSize[T, F xmath.Numeric](s Size[F]) Size[T] { 34 | return NewSize(T(s.Width), T(s.Height)) 35 | } 36 | 37 | // Add returns a new Size which is the result of adding this Size with the provided Size. 38 | func (s Size[T]) Add(size Size[T]) Size[T] { 39 | return Size[T]{Width: s.Width + size.Width, Height: s.Height + size.Height} 40 | } 41 | 42 | // Sub returns a new Size which is the result of subtracting the provided Size from this Size. 43 | func (s Size[T]) Sub(size Size[T]) Size[T] { 44 | return Size[T]{Width: s.Width - size.Width, Height: s.Height - size.Height} 45 | } 46 | 47 | // Mul returns a new Size which is the result of multiplying this Size by the value. 48 | func (s Size[T]) Mul(value T) Size[T] { 49 | return Size[T]{Width: s.Width * value, Height: s.Height * value} 50 | } 51 | 52 | // Div returns a new Size which is the result of dividing this Size by the value. 53 | func (s Size[T]) Div(value T) Size[T] { 54 | return Size[T]{Width: s.Width / value, Height: s.Height / value} 55 | } 56 | 57 | // Floor returns a new Size with its width and height floored. 58 | func (s Size[T]) Floor() Size[T] { 59 | return Size[T]{Width: xmath.Floor(s.Width), Height: xmath.Floor(s.Height)} 60 | } 61 | 62 | // Ceil returns a new Size with its width and height ceiled. 63 | func (s Size[T]) Ceil() Size[T] { 64 | return Size[T]{Width: xmath.Ceil(s.Width), Height: xmath.Ceil(s.Height)} 65 | } 66 | 67 | // Min returns the smallest Size between itself and 'other'. 68 | func (s Size[T]) Min(other Size[T]) Size[T] { 69 | return Size[T]{Width: min(s.Width, other.Width), Height: min(s.Height, other.Height)} 70 | } 71 | 72 | // Max returns the largest Size between itself and 'other'. 73 | func (s Size[T]) Max(other Size[T]) Size[T] { 74 | return Size[T]{Width: max(s.Width, other.Width), Height: max(s.Height, other.Height)} 75 | } 76 | 77 | // ConstrainForHint returns a size no larger than the hint value. Hint values less than one are ignored. 78 | func (s Size[T]) ConstrainForHint(hint Size[T]) Size[T] { 79 | w := s.Width 80 | if hint.Width >= 1 && w > hint.Width { 81 | w = hint.Width 82 | } 83 | h := s.Height 84 | if hint.Height >= 1 && h > hint.Height { 85 | h = hint.Height 86 | } 87 | return Size[T]{Width: w, Height: h} 88 | } 89 | 90 | // String implements fmt.Stringer. 91 | func (s Size[T]) String() string { 92 | return fmt.Sprintf("%#v,%#v", s.Width, s.Height) 93 | } 94 | -------------------------------------------------------------------------------- /xmath/geom/visibility/README.md: -------------------------------------------------------------------------------- 1 | # Warning! 2 | 3 | The visibility code doesn't function as expected in all cases. It does work for most cases, but I 4 | recommend against using it in production code at the moment. -------------------------------------------------------------------------------- /xmath/geom/visibility/array.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | package visibility 11 | 12 | type array struct { 13 | data []int 14 | } 15 | 16 | func (a *array) size() int { 17 | return len(a.data) 18 | } 19 | 20 | func (a *array) elem(index int) int { 21 | return a.data[index] 22 | } 23 | 24 | func (a *array) set(index, value int) { 25 | a.data[index] = value 26 | } 27 | 28 | func (a *array) pop() int { 29 | v := a.data[len(a.data)-1] 30 | a.data = a.data[:len(a.data)-1] 31 | return v 32 | } 33 | 34 | func (a *array) push(v int) { 35 | a.data = append(a.data, v) 36 | } 37 | -------------------------------------------------------------------------------- /xmath/geom/visibility/endpoint.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | package visibility 11 | 12 | import ( 13 | "github.com/richardwilkes/toolbox/xmath/geom" 14 | "golang.org/x/exp/constraints" 15 | ) 16 | 17 | type endPoint[T constraints.Float] struct { 18 | angle T 19 | segmentIndex int 20 | start bool 21 | } 22 | 23 | func (ep *endPoint[T]) pt(segments []Segment[T]) geom.Point[T] { 24 | if ep.start { 25 | return segments[ep.segmentIndex].Start 26 | } 27 | return segments[ep.segmentIndex].End 28 | } 29 | -------------------------------------------------------------------------------- /xmath/geom/visibility/segment.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | package visibility 11 | 12 | import ( 13 | "github.com/richardwilkes/toolbox/xmath/geom" 14 | "golang.org/x/exp/constraints" 15 | ) 16 | 17 | // Segment holds the start and end points of a line. 18 | type Segment[T constraints.Float] struct { 19 | Start geom.Point[T] 20 | End geom.Point[T] 21 | } 22 | 23 | // Bounds returns the bounding rectangle of this Segment. This includes a slight bit of expansion to compensate for 24 | // floating-point imprecision. 25 | func (s Segment[T]) Bounds() geom.Rect[T] { 26 | minX := min(s.Start.X, s.End.X) 27 | minY := min(s.Start.Y, s.End.Y) 28 | return geom.NewRect[T](minX-epsilon, minY-epsilon, max(s.Start.X, s.End.X)-minX+epsilon*2, 29 | max(s.Start.Y, s.End.Y)-minY+epsilon*2) 30 | } 31 | -------------------------------------------------------------------------------- /xmath/hashhelper/hashhelper.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | package hashhelper 11 | 12 | import ( 13 | "hash" 14 | "math" 15 | ) 16 | 17 | // String writes the given string to the hash. 18 | func String[T ~string](h hash.Hash, data T) { 19 | Num64(h, len(data)) 20 | _, _ = h.Write([]byte(data)) 21 | } 22 | 23 | // Num64 writes the given 64-bit number to the hash. 24 | func Num64[T ~int64 | ~uint64 | ~int | ~uint](h hash.Hash, data T) { 25 | var buffer [8]byte 26 | d := uint64(data) 27 | buffer[0] = byte(d) 28 | buffer[1] = byte(d >> 8) 29 | buffer[2] = byte(d >> 16) 30 | buffer[3] = byte(d >> 24) 31 | buffer[4] = byte(d >> 32) 32 | buffer[5] = byte(d >> 40) 33 | buffer[6] = byte(d >> 48) 34 | buffer[7] = byte(d >> 56) 35 | _, _ = h.Write(buffer[:]) 36 | } 37 | 38 | // Num32 writes the given 32-bit number to the hash. 39 | func Num32[T ~int32 | ~uint32](h hash.Hash, data T) { 40 | var buffer [4]byte 41 | d := uint32(data) 42 | buffer[0] = byte(d) 43 | buffer[1] = byte(d >> 8) 44 | buffer[2] = byte(d >> 16) 45 | buffer[3] = byte(d >> 24) 46 | _, _ = h.Write(buffer[:]) 47 | } 48 | 49 | // Num16 writes the given 16-bit number to the hash. 50 | func Num16[T ~int16 | ~uint16](h hash.Hash, data T) { 51 | var buffer [2]byte 52 | d := uint16(data) 53 | buffer[0] = byte(d) 54 | buffer[1] = byte(d >> 8) 55 | _, _ = h.Write(buffer[:]) 56 | } 57 | 58 | // Num8 writes the given 8-bit number to the hash. 59 | func Num8[T ~int8 | ~uint8](h hash.Hash, data T) { 60 | _, _ = h.Write([]byte{byte(data)}) 61 | } 62 | 63 | // Bool writes the given boolean to the hash. 64 | func Bool[T ~bool](h hash.Hash, data T) { 65 | var b byte 66 | if data { 67 | b = 1 68 | } 69 | _, _ = h.Write([]byte{b}) 70 | } 71 | 72 | // Float64 writes the given 64-bit float to the hash. 73 | func Float64[T ~float64](h hash.Hash, data T) { 74 | Num64(h, math.Float64bits(float64(data))) 75 | } 76 | 77 | // Float32 writes the given 64-bit float to the hash. 78 | func Float32[T ~float32](h hash.Hash, data T) { 79 | Num32(h, math.Float32bits(float32(data))) 80 | } 81 | -------------------------------------------------------------------------------- /xmath/rand/random.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 | // 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, version 2.0. If a copy of the MPL was not distributed with 5 | // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | // 7 | // This Source Code Form is "Incompatible With Secondary Licenses", as 8 | // defined by the Mozilla Public License, version 2.0. 9 | 10 | // Package rand provides a Randomizer based upon the crypto/rand package. 11 | package rand 12 | 13 | import ( 14 | "crypto/rand" 15 | mrnd "math/rand/v2" 16 | ) 17 | 18 | var cryptoRandInstance = &cryptoRand{} 19 | 20 | // Randomizer defines a source of random integer values. 21 | type Randomizer interface { 22 | // Intn returns a non-negative random number from 0 to n-1. If n <= 0, the implementation should return 0. 23 | Intn(n int) int 24 | } 25 | 26 | // NewCryptoRand returns a Randomizer based on the crypto/rand package. This method returns a shared singleton instance 27 | // and does not allocate. 28 | func NewCryptoRand() Randomizer { 29 | return cryptoRandInstance 30 | } 31 | 32 | type cryptoRand struct{} 33 | 34 | func (r *cryptoRand) Intn(n int) int { 35 | if n <= 0 { 36 | return 0 37 | } 38 | var buffer [8]byte 39 | size := 8 40 | n64 := int64(n) 41 | for i := 1; i < 8; i++ { 42 | if n64 < int64(1)<= 0; i-- { 52 | v |= int(buffer[i]) << uint(i*8) 53 | } 54 | if v < 0 { 55 | v = -v 56 | } 57 | return v % n 58 | } 59 | --------------------------------------------------------------------------------