diff options
| author | Dave Henderson <dhenderson@gmail.com> | 2019-02-28 20:34:31 -0500 |
|---|---|---|
| committer | Dave Henderson <dhenderson@gmail.com> | 2019-03-17 13:00:23 -0400 |
| commit | 0347dd1f69480206fb23ff754cbf5b459bf06817 (patch) | |
| tree | 91cc47762c0cb13c53d66d969fda29c511523a5a | |
| parent | 13c1c4bf680acb6c9421c30c72ec21a626897b70 (diff) | |
New random namespace for generating random strings and numbers
Signed-off-by: Dave Henderson <dhenderson@gmail.com>
| -rw-r--r-- | docs-src/content/functions/random.yml | 169 | ||||
| -rw-r--r-- | docs/content/functions/random.md | 251 | ||||
| -rw-r--r-- | funcs.go | 1 | ||||
| -rw-r--r-- | funcs/random.go | 162 | ||||
| -rw-r--r-- | funcs/random_test.go | 175 | ||||
| -rw-r--r-- | random/random.go | 109 | ||||
| -rw-r--r-- | random/random_test.go | 127 |
7 files changed, 994 insertions, 0 deletions
diff --git a/docs-src/content/functions/random.yml b/docs-src/content/functions/random.yml new file mode 100644 index 00000000..b4f8c4cd --- /dev/null +++ b/docs-src/content/functions/random.yml @@ -0,0 +1,169 @@ +ns: random +title: random functions +preamble: | + Functions for generating random values. + + ### About randomness + + `gomplate` uses Go's [`math/rand`](https://golang.org/pkg/math/rand/) package + to generate pseudo-random numbers. Note that these functions are not suitable + for use in security-sensitive applications, such as cryptography. However, + these functions will not deplete system entropy. +funcs: + - name: random.ASCII + description: | + Generates a random string of a desired length, containing the set of + printable characters from the 7-bit [ASCII](https://en.wikipedia.org/wiki/ASCII) + set. This includes _space_ (' '), but no other whitespace characters. + pipeline: false + arguments: + - name: count + required: true + description: the length of the string to produce (number of characters) + examples: + - | + $ gomplate -i '{{ random.ASCII 8 }}' + _woJ%D&K + - name: random.Alpha + description: | + Generates a random alphabetical (`A-Z`, `a-z`) string of a desired length. + pipeline: false + arguments: + - name: count + required: true + description: the length of the string to produce (number of characters) + examples: + - | + $ gomplate -i '{{ random.Alpha 42 }}' + oAqHKxHiytYicMxTMGHnUnAfltPVZDhFkVkgDvatJK + - name: random.AlphaNum + description: | + Generates a random alphanumeric (`0-9`, `A-Z`, `a-z`) string of a desired length. + pipeline: false + arguments: + - name: count + required: true + description: the length of the string to produce (number of characters) + examples: + - | + $ gomplate -i '{{ random.AlphaNum 16 }}' + 4olRl9mRmVp1nqSm + - name: random.String + description: | + Generates a random string of a desired length. + + By default, the possible characters are those represented by the + regular expression `[a-zA-Z0-9_.-]` (alphanumeric, plus `_`, `.`, and `-`). + + A different set of characters can be specified with a regular expression, + or by giving a range of possible characters by specifying the lower and + upper bounds. Lower/upper bounds can be specified as characters (e.g. + `"q"`, or escape sequences such as `"\U0001f0AF"`), or numeric Unicode + code-points (e.g. `48` or `0x30` for the character `0`). + + When given a range of Unicode code-points, `random.String` will discard + non-printable characters from the selection. This may result in a much + smaller set of possible characters than intended, so check + the [Unicode character code charts](http://www.unicode.org/charts/) to + verify the correct code-points. + pipeline: false + arguments: + - name: count + required: true + description: the length of the string to produce (number of characters) + - name: regex + required: false + description: the regular expression that each character must match (defaults to `[a-zA-Z0-9_.-]`) + - name: lower + required: false + description: lower bound for a range of characters (number or single character) + - name: upper + required: false + description: upper bound for a range of characters (number or single character) + examples: + - | + $ gomplate -i '{{ random.String 8 }}' + FODZ01u_ + - | + $ gomplate -i '{{ random.String 16 `[[:xdigit:]]` }}' + B9e0527C3e45E1f3 + - | + $ gomplate -i '{{ random.String 20 `[\p{Canadian_Aboriginal}]` }}' + ᗄᖖᣡᕔᕫᗝᖴᒙᗌᘔᓰᖫᗵᐕᗵᙔᗠᓅᕎᔹ + - | + $ gomplate -i '{{ random.String 8 "c" "m" }}' + ffmidgjc + - | + $ gomplate -i 'You rolled... {{ random.String 3 "⚀" "⚅" }}' + You rolled... ⚅⚂⚁ + - | + $ gomplate -i 'Poker time! {{ random.String 5 "\U0001f0a1" "\U0001f0de" }}' + Poker time! 🂼🂺🂳🃅🂪 + - name: random.Item + description: | + Pick an element at a random from a given slice or array. + pipeline: true + arguments: + - name: items + required: true + description: the input array + examples: + - | + $ gomplate -i '{{ random.Item (seq 0 5) }}' + 4 + - | + $ export SLICE='["red", "green", "blue"]' + $ gomplate -i '{{ getenv "SLICE" | jsonArray | random.Item }}' + blue + - name: random.Number + description: | + Pick a random integer. By default, a number between `0` and `100` + (inclusive) is chosen, but this range can be overridden. + + Note that the difference between `min` and `max` can not be larger than a + 63-bit integer (i.e. the unsigned portion of a 64-bit signed integer). + The result is given as an `int64`. + pipeline: false + arguments: + - name: min + required: false + description: The minimum value, defaults to `0`. Must be less than `max`. + - name: max + required: false + description: The maximum value, defaults to `100` (if no args provided) + examples: + - | + $ gomplate -i '{{ random.Number }}' + 55 + - | + $ gomplate -i '{{ random.Number -10 10 }}' + -3 + - | + $ gomplate -i '{{ random.Number 5 }}' + 2 + - name: random.Float + description: | + Pick a random decimal floating-point number. By default, a number between + `0.0` and `1.0` (_exclusive_, i.e. `[0.0,1.0)`) is chosen, but this range + can be overridden. + + The result is given as a `float64`. + pipeline: false + arguments: + - name: min + required: false + description: The minimum value, defaults to `0.0`. Must be less than `max`. + - name: max + required: false + description: The maximum value, defaults to `1.0` (if no args provided). + examples: + - | + $ gomplate -i '{{ random.Float }}' + 0.2029946480303966 + - | + $ gomplate -i '{{ random.Float 100 }}' + 71.28595374161743 + - | + $ gomplate -i '{{ random.Float -100 200 }}' + 105.59119437834909 +
\ No newline at end of file diff --git a/docs/content/functions/random.md b/docs/content/functions/random.md new file mode 100644 index 00000000..8396e0f9 --- /dev/null +++ b/docs/content/functions/random.md @@ -0,0 +1,251 @@ +--- +title: random functions +menu: + main: + parent: functions +--- + +Functions for generating random values. + +### About randomness + +`gomplate` uses Go's [`math/rand`](https://golang.org/pkg/math/rand/) package +to generate pseudo-random numbers. Note that these functions are not suitable +for use in security-sensitive applications, such as cryptography. However, +these functions will not deplete system entropy. + +## `random.ASCII` + +Generates a random string of a desired length, containing the set of +printable characters from the 7-bit [ASCII](https://en.wikipedia.org/wiki/ASCII) +set. This includes _space_ (' '), but no other whitespace characters. + +### Usage + +```go +random.ASCII count +``` + +### Arguments + +| name | description | +|------|-------------| +| `count` | _(required)_ the length of the string to produce (number of characters) | + +### Examples + +```console +$ gomplate -i '{{ random.ASCII 8 }}' +_woJ%D&K +``` + +## `random.Alpha` + +Generates a random alphabetical (`A-Z`, `a-z`) string of a desired length. + +### Usage + +```go +random.Alpha count +``` + +### Arguments + +| name | description | +|------|-------------| +| `count` | _(required)_ the length of the string to produce (number of characters) | + +### Examples + +```console +$ gomplate -i '{{ random.Alpha 42 }}' +oAqHKxHiytYicMxTMGHnUnAfltPVZDhFkVkgDvatJK +``` + +## `random.AlphaNum` + +Generates a random alphanumeric (`0-9`, `A-Z`, `a-z`) string of a desired length. + +### Usage + +```go +random.AlphaNum count +``` + +### Arguments + +| name | description | +|------|-------------| +| `count` | _(required)_ the length of the string to produce (number of characters) | + +### Examples + +```console +$ gomplate -i '{{ random.AlphaNum 16 }}' +4olRl9mRmVp1nqSm +``` + +## `random.String` + +Generates a random string of a desired length. + +By default, the possible characters are those represented by the +regular expression `[a-zA-Z0-9_.-]` (alphanumeric, plus `_`, `.`, and `-`). + +A different set of characters can be specified with a regular expression, +or by giving a range of possible characters by specifying the lower and +upper bounds. Lower/upper bounds can be specified as characters (e.g. +`"q"`, or escape sequences such as `"\U0001f0AF"`), or numeric Unicode +code-points (e.g. `48` or `0x30` for the character `0`). + +When given a range of Unicode code-points, `random.String` will discard +non-printable characters from the selection. This may result in a much +smaller set of possible characters than intended, so check +the [Unicode character code charts](http://www.unicode.org/charts/) to +verify the correct code-points. + +### Usage + +```go +random.String count [regex] [lower] [upper] +``` + +### Arguments + +| name | description | +|------|-------------| +| `count` | _(required)_ the length of the string to produce (number of characters) | +| `regex` | _(optional)_ the regular expression that each character must match (defaults to `[a-zA-Z0-9_.-]`) | +| `lower` | _(optional)_ lower bound for a range of characters (number or single character) | +| `upper` | _(optional)_ upper bound for a range of characters (number or single character) | + +### Examples + +```console +$ gomplate -i '{{ random.String 8 }}' +FODZ01u_ +``` +```console +$ gomplate -i '{{ random.String 16 `[[:xdigit:]]` }}' +B9e0527C3e45E1f3 +``` +```console +$ gomplate -i '{{ random.String 20 `[\p{Canadian_Aboriginal}]` }}' +ᗄᖖᣡᕔᕫᗝᖴᒙᗌᘔᓰᖫᗵᐕᗵᙔᗠᓅᕎᔹ +``` +```console +$ gomplate -i '{{ random.String 8 "c" "m" }}' +ffmidgjc +``` +```console +$ gomplate -i 'You rolled... {{ random.String 3 "⚀" "⚅" }}' +You rolled... ⚅⚂⚁ +``` +```console +$ gomplate -i 'Poker time! {{ random.String 5 "\U0001f0a1" "\U0001f0de" }}' +Poker time! 🂼🂺🂳🃅🂪 +``` + +## `random.Item` + +Pick an element at a random from a given slice or array. + +### Usage + +```go +random.Item items +``` +```go +items | random.Item +``` + +### Arguments + +| name | description | +|------|-------------| +| `items` | _(required)_ the input array | + +### Examples + +```console +$ gomplate -i '{{ random.Item (seq 0 5) }}' +4 +``` +```console +$ export SLICE='["red", "green", "blue"]' +$ gomplate -i '{{ getenv "SLICE" | jsonArray | random.Item }}' +blue +``` + +## `random.Number` + +Pick a random integer. By default, a number between `0` and `100` +(inclusive) is chosen, but this range can be overridden. + +Note that the difference between `min` and `max` can not be larger than a +63-bit integer (i.e. the unsigned portion of a 64-bit signed integer). +The result is given as an `int64`. + +### Usage + +```go +random.Number [min] [max] +``` + +### Arguments + +| name | description | +|------|-------------| +| `min` | _(optional)_ The minimum value, defaults to `0`. Must be less than `max`. | +| `max` | _(optional)_ The maximum value, defaults to `100` (if no args provided) | + +### Examples + +```console +$ gomplate -i '{{ random.Number }}' +55 +``` +```console +$ gomplate -i '{{ random.Number -10 10 }}' +-3 +``` +```console +$ gomplate -i '{{ random.Number 5 }}' +2 +``` + +## `random.Float` + +Pick a random decimal floating-point number. By default, a number between +`0.0` and `1.0` (_exclusive_, i.e. `[0.0,1.0)`) is chosen, but this range +can be overridden. + +The result is given as a `float64`. + +### Usage + +```go +random.Float [min] [max] +``` + +### Arguments + +| name | description | +|------|-------------| +| `min` | _(optional)_ The minimum value, defaults to `0.0`. Must be less than `max`. | +| `max` | _(optional)_ The maximum value, defaults to `1.0` (if no args provided). | + +### Examples + +```console +$ gomplate -i '{{ random.Float }}' +0.2029946480303966 +``` +```console +$ gomplate -i '{{ random.Float 100 }}' +71.28595374161743 +``` +```console +$ gomplate -i '{{ random.Float -100 200 }}' +105.59119437834909 +``` @@ -28,5 +28,6 @@ func Funcs(d *data.Data) template.FuncMap { funcs.AddTestFuncs(f) funcs.AddCollFuncs(f) funcs.AddUUIDFuncs(f) + funcs.AddRandomFuncs(f) return f } diff --git a/funcs/random.go b/funcs/random.go new file mode 100644 index 00000000..d4dce3e7 --- /dev/null +++ b/funcs/random.go @@ -0,0 +1,162 @@ +package funcs + +import ( + "reflect" + "strconv" + "sync" + "unicode/utf8" + + "github.com/hairyhenderson/gomplate/conv" + "github.com/hairyhenderson/gomplate/random" + "github.com/pkg/errors" +) + +var ( + randomNS *RandomFuncs + randomNSInit sync.Once +) + +// RandomNS - +func RandomNS() *RandomFuncs { + randomNSInit.Do(func() { randomNS = &RandomFuncs{} }) + return randomNS +} + +// AddRandomFuncs - +func AddRandomFuncs(f map[string]interface{}) { + f["random"] = RandomNS +} + +// RandomFuncs - +type RandomFuncs struct{} + +// ASCII - +func (f *RandomFuncs) ASCII(count interface{}) (string, error) { + return random.StringBounds(conv.ToInt(count), ' ', '~') +} + +// Alpha - +func (f *RandomFuncs) Alpha(count interface{}) (string, error) { + return random.StringRE(conv.ToInt(count), "[[:alpha:]]") +} + +// AlphaNum - +func (f *RandomFuncs) AlphaNum(count interface{}) (string, error) { + return random.StringRE(conv.ToInt(count), "[[:alnum:]]") +} + +// String - +func (f *RandomFuncs) String(count interface{}, args ...interface{}) (s string, err error) { + c := conv.ToInt(count) + if c == 0 { + return "", errors.New("count must be greater than 0") + } + m := "" + switch len(args) { + case 0: + m = "" + case 1: + m = conv.ToString(args[0]) + case 2: + var l, u rune + if isString(args[0]) && isString(args[1]) { + l, u, err = toCodePoints(args[0].(string), args[1].(string)) + if err != nil { + return "", err + } + } else { + l = rune(conv.ToInt(args[0])) + u = rune(conv.ToInt(args[1])) + } + + return random.StringBounds(c, l, u) + } + + return random.StringRE(c, m) +} + +func isString(s interface{}) bool { + switch s.(type) { + case string: + return true + default: + return false + } +} + +var rlen = utf8.RuneCountInString + +func toCodePoints(l, u string) (rune, rune, error) { + // no way are these representing valid printable codepoints - we'll treat + // them as runes + if rlen(l) == rlen(u) && rlen(l) == 1 { + lower, _ := utf8.DecodeRuneInString(l) + upper, _ := utf8.DecodeRuneInString(u) + return lower, upper, nil + } + + li, err := strconv.ParseInt(l, 0, 32) + if err != nil { + return 0, 0, err + } + ui, err := strconv.ParseInt(u, 0, 32) + if err != nil { + return 0, 0, err + } + + return rune(li), rune(ui), nil +} + +func interfaceSlice(slice interface{}) ([]interface{}, error) { + s := reflect.ValueOf(slice) + kind := s.Kind() + switch kind { + case reflect.Slice, reflect.Array: + ret := make([]interface{}, s.Len()) + for i := 0; i < s.Len(); i++ { + ret[i] = s.Index(i).Interface() + } + return ret, nil + default: + return nil, errors.Errorf("expected an array or slice, but got a %T", s) + } +} + +// Item - +func (f *RandomFuncs) Item(items interface{}) (interface{}, error) { + i, err := interfaceSlice(items) + if err != nil { + return nil, err + } + return random.Item(i) +} + +// Number - +func (f *RandomFuncs) Number(args ...interface{}) (int64, error) { + var min, max int64 + min, max = 0, 100 + switch len(args) { + case 0: + case 1: + max = conv.ToInt64(args[0]) + case 2: + min = conv.ToInt64(args[0]) + max = conv.ToInt64(args[1]) + } + return random.Number(min, max) +} + +// Float - +func (f *RandomFuncs) Float(args ...interface{}) (float64, error) { + var min, max float64 + min, max = 0, 1.0 + switch len(args) { + case 0: + case 1: + max = conv.ToFloat64(args[0]) + case 2: + min = conv.ToFloat64(args[0]) + max = conv.ToFloat64(args[1]) + } + return random.Float(min, max) +} diff --git a/funcs/random_test.go b/funcs/random_test.go new file mode 100644 index 00000000..8f1a5a2f --- /dev/null +++ b/funcs/random_test.go @@ -0,0 +1,175 @@ +package funcs + +import ( + "testing" + "unicode/utf8" + + "github.com/stretchr/testify/assert" +) + +func TestASCII(t *testing.T) { + f := &RandomFuncs{} + s, err := f.ASCII(0) + assert.NoError(t, err) + assert.Empty(t, s) + + s, err = f.ASCII(100) + assert.NoError(t, err) + assert.Len(t, s, 100) + assert.Regexp(t, "^[[:print:]]*$", s) +} + +func TestAlpha(t *testing.T) { + f := &RandomFuncs{} + s, err := f.Alpha(0) + assert.NoError(t, err) + assert.Empty(t, s) + + s, err = f.Alpha(100) + assert.NoError(t, err) + assert.Len(t, s, 100) + assert.Regexp(t, "^[[:alpha:]]*$", s) +} + +func TestAlphaNum(t *testing.T) { + f := &RandomFuncs{} + s, err := f.AlphaNum(0) + assert.NoError(t, err) + assert.Empty(t, s) + + s, err = f.AlphaNum(100) + assert.NoError(t, err) + assert.Len(t, s, 100) + assert.Regexp(t, "^[[:alnum:]]*$", s) +} + +func TestToCodePoints(t *testing.T) { + l, u, err := toCodePoints("a", "b") + assert.NoError(t, err) + assert.Equal(t, 'a', l) + assert.Equal(t, 'b', u) + + _, _, err = toCodePoints("foo", "bar") + assert.Error(t, err) + + _, _, err = toCodePoints("0755", "bar") + assert.Error(t, err) + + l, u, err = toCodePoints("0xD700", "0x0001FFFF") + assert.NoError(t, err) + assert.Equal(t, '\ud700', l) + assert.Equal(t, '\U0001ffff', u) + + l, u, err = toCodePoints("0011", "0777") + assert.NoError(t, err) + assert.Equal(t, rune(0011), l) + assert.Equal(t, rune(0777), u) + + l, u, err = toCodePoints("♬", "♟") + assert.NoError(t, err) + assert.Equal(t, rune(0x266C), l) + assert.Equal(t, '♟', u) +} + +func TestString(t *testing.T) { + f := &RandomFuncs{} + out, err := f.String(1) + assert.NoError(t, err) + assert.Len(t, out, 1) + + out, err = f.String(42) + assert.NoError(t, err) + assert.Len(t, out, 42) + + _, err = f.String(0) + assert.Error(t, err) + + out, err = f.String(8, "[a-z]") + assert.NoError(t, err) + assert.Regexp(t, "^[a-z]{8}$", out) + + out, err = f.String(10, 0x23, 0x26) + assert.NoError(t, err) + assert.Regexp(t, "^[#$%&]{10}$", out) + + out, err = f.String(8, '\U0001f062', '\U0001f093') + assert.NoError(t, err) + assert.Regexp(t, "^[🁢-🂓]{8}$", out) + + out, err = f.String(8, '\U0001f062', '\U0001f093') + assert.NoError(t, err) + assert.Regexp(t, "^[🁢-🂓]{8}$", out) + + out, err = f.String(8, "♚", "♟") + assert.NoError(t, err) + assert.Regexp(t, "^[♚-♟]{8}$", out) + + out, err = f.String(100, "♠", "♣") + assert.NoError(t, err) + assert.Equal(t, 100, utf8.RuneCountInString(out)) + assert.Regexp(t, "^[♠-♣]{100}$", out) +} + +func TestItem(t *testing.T) { + f := &RandomFuncs{} + _, err := f.Item(nil) + assert.Error(t, err) + + _, err = f.Item("foo") + assert.Error(t, err) + + i, err := f.Item([]string{"foo"}) + assert.NoError(t, err) + assert.Equal(t, "foo", i) + + in := []string{"foo", "bar"} + got := "" + for j := 0; j < 10; j++ { + i, err = f.Item(in) + assert.NoError(t, err) + got += i.(string) + } + assert.NotEqual(t, "foofoofoofoofoofoofoofoofoofoo", got) + assert.NotEqual(t, "barbarbarbarbarbarbarbarbarbar", got) +} + +func TestNumber(t *testing.T) { + f := &RandomFuncs{} + n, err := f.Number() + assert.NoError(t, err) + assert.True(t, 0 < n && n < 100) + + _, err = f.Number(-1) + assert.Error(t, err) + + n, err = f.Number(0) + assert.NoError(t, err) + assert.Equal(t, int64(0), n) + + n, err = f.Number(9, 9) + assert.NoError(t, err) + assert.Equal(t, int64(9), n) + + n, err = f.Number(-10, -10) + assert.NoError(t, err) + assert.Equal(t, int64(-10), n) +} + +func TestFloat(t *testing.T) { + f := &RandomFuncs{} + n, err := f.Float() + assert.NoError(t, err) + assert.InDelta(t, 0.5, n, 0.5) + + n, err = f.Float(0.5) + assert.NoError(t, err) + assert.InDelta(t, 0.25, n, 0.25) + + n, err = f.Float(490, 500) + assert.NoError(t, err) + assert.InDelta(t, 495, n, 5) + + n, err = f.Float(-500, 500) + assert.NoError(t, err) + assert.InDelta(t, 0, n, 500) +} diff --git a/random/random.go b/random/random.go new file mode 100644 index 00000000..58a44d7d --- /dev/null +++ b/random/random.go @@ -0,0 +1,109 @@ +package random + +import ( + "math" + "math/rand" + "regexp" + "time" + "unicode" + + "github.com/pkg/errors" +) + +// Rnd - +var Rnd = rand.New(rand.NewSource(time.Now().UnixNano())) + +// Default set, matches "[a-zA-Z0-9_.-]" +const defaultSet = "-.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz" + +// StringRE - Generate a random string that matches a given regular +// expression. Defaults to "[a-zA-Z0-9_.-]" +func StringRE(count int, match string) (r string, err error) { + var chars []rune + chars = []rune(defaultSet) + if match != "" { + chars, err = matchChars(match) + if err != nil { + return "", err + } + } + + return rndString(count, chars) +} + +// StringBounds returns a random string of characters with a codepoint +// between the lower and upper bounds. Only valid characters are returned +// and if a range is given where no valid characters can be found, an error +// will be returned. +func StringBounds(count int, lower, upper rune) (r string, err error) { + chars := filterRange(lower, upper) + if len(chars) == 0 { + return "", errors.Errorf("No printable codepoints found between U%#q and U%#q.", lower, upper) + } + return rndString(count, chars) +} + +// produce a string containing a random selection of given characters +func rndString(count int, chars []rune) (string, error) { + s := make([]rune, count) + for i := range s { + s[i] = chars[Rnd.Intn(len(chars))] + } + return string(s), nil +} + +func filterRange(lower, upper rune) []rune { + out := []rune{} + for r := lower; r <= upper; r++ { + if unicode.IsGraphic(r) { + out = append(out, r) + } + } + return out +} + +func matchChars(match string) ([]rune, error) { + r, err := regexp.Compile(match) + if err != nil { + return nil, err + } + candidates := filterRange(0, unicode.MaxRune) + out := []rune{} + for _, c := range candidates { + if r.MatchString(string(c)) { + out = append(out, c) + } + } + return out, nil +} + +// Item - +func Item(items []interface{}) (interface{}, error) { + if len(items) == 0 { + return nil, errors.Errorf("expected a non-empty array or slice") + } + if len(items) == 1 { + return items[0], nil + } + n := Rnd.Intn(len(items)) + return items[n], nil +} + +// Number - +func Number(min, max int64) (int64, error) { + if min > max { + return 0, errors.Errorf("min must not be greater than max (was %d, %d)", min, max) + } + if min == math.MinInt64 { + min++ + } + if max-min >= (math.MaxInt64 >> 1) { + return 0, errors.Errorf("spread between min and max too high - must not be greater than 63-bit maximum (%d - %d = %d)", max, min, max-min) + } + return Rnd.Int63n(max-min+1) + min, nil +} + +// Float - For now this is really just a wrapper around `rand.Float64` +func Float(min, max float64) (float64, error) { + return min + Rnd.Float64()*(max-min), nil +} diff --git a/random/random_test.go b/random/random_test.go new file mode 100644 index 00000000..7b6b49af --- /dev/null +++ b/random/random_test.go @@ -0,0 +1,127 @@ +package random + +import ( + "math" + "testing" + "unicode/utf8" + + "github.com/stretchr/testify/assert" +) + +func TestMatchChars(t *testing.T) { + in := "[a-g]" + expected := []rune("abcdefg") + out, err := matchChars(in) + assert.NoError(t, err) + assert.EqualValues(t, expected, out) + + in = "[a-zA-Z0-9_.-]" + expected = []rune(defaultSet) + out, err = matchChars(in) + assert.NoError(t, err) + assert.Equal(t, expected, out) + + in = "[[:alpha:]]" + expected = []rune("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz") + out, err = matchChars(in) + assert.NoError(t, err) + assert.Equal(t, expected, out) +} + +func TestStringRE(t *testing.T) { + r, err := StringRE(15, "[\\p{Yi}[:alnum:]]") + assert.NoError(t, err) + assert.Equal(t, 15, utf8.RuneCountInString(r)) + + _, err = StringRE(1, "[bogus") + assert.Error(t, err) +} + +func TestStringBounds(t *testing.T) { + _, err := StringBounds(15, 0, 19) + assert.Error(t, err) + + // surrogate range isn't valid, should error + _, err = StringBounds(15, 0xd800, 0xdfff) + assert.Error(t, err) + + r, err := StringBounds(1, 'a', 'a') + assert.NoError(t, err) + assert.Equal(t, "a", r) + + r, err = StringBounds(99, 'a', 'b') + assert.NoError(t, err) + assert.Regexp(t, "^[a-b]+$", r) + + r, err = StringBounds(100, 0x0020, 0x007f) + assert.NoError(t, err) + assert.Regexp(t, "^[\u0020-\u007f]*$", r) + + // only 🂱 (\U0001F0B1) in this range is "graphic" + r, err = StringBounds(8, 0x0001f0af, 0x0001f0b1) + assert.NoError(t, err) + assert.Regexp(t, "^🂱🂱🂱🂱🂱🂱🂱🂱$", r) +} + +func TestItem(t *testing.T) { + _, err := Item(nil) + assert.Error(t, err) + + i, err := Item([]interface{}{"foo"}) + assert.NoError(t, err) + assert.Equal(t, "foo", i) + + in := []interface{}{"foo", "bar"} + got := "" + for j := 0; j < 10; j++ { + i, err = Item(in) + assert.NoError(t, err) + got += i.(string) + } + assert.NotEqual(t, "foofoofoofoofoofoofoofoofoofoo", got) + assert.NotEqual(t, "barbarbarbarbarbarbarbarbarbar", got) +} + +func TestNumber(t *testing.T) { + _, err := Number(0, -1) + assert.Error(t, err) + _, err = Number(0, math.MaxInt64) + assert.Error(t, err) + _, err = Number(math.MinInt64, 0) + assert.Error(t, err) + + testdata := []struct { + min, max, expected int64 + delta float64 + }{ + {0, 100, 50, 50}, + {0, 0, 0, 0}, + {9, 9, 9, 0}, + {-10, -10, -10, 0}, + {-10, -0, -5, 5}, + } + for _, d := range testdata { + n, err := Number(d.min, d.max) + assert.NoError(t, err) + assert.InDelta(t, d.expected, n, d.delta) + } +} + +func TestFloat(t *testing.T) { + testdata := []struct { + min, max, expected float64 + delta float64 + }{ + {0, 1.0, 0.5, 0.5}, + {0, 0.5, 0.25, 0.25}, + {490, 500, 495, 5}, + {-500, 500, 0, 500}, + {0, math.MaxFloat64, math.MaxFloat64 / 2, math.MaxFloat64 / 2}, + } + + for _, d := range testdata { + n, err := Float(d.min, d.max) + assert.NoError(t, err) + assert.InDelta(t, d.expected, n, d.delta) + } +} |
