Browse Source

Range support

Added support for version ranges with basic operators:

v, err := semver.Parse("1.2.3")
range, err := semver.ParseRange(">1.0.0 <2.0.0 || >=3.0.0")
if range(v) {
    //valid
}
Benedikt Lang 10 years ago
parent
commit
aea32c919a
3 changed files with 729 additions and 14 deletions
  1. +63
    -14
      README.md
  2. +224
    -0
      range.go
  3. +442
    -0
      range_test.go

+ 63
- 14
README.md View File

@ -40,10 +40,52 @@ Features
- Comparator-like comparisons
- Compare Helper Methods
- InPlace manipulation
- Ranges `>=1.0.0 <2.0.0 || >=3.0.0 !3.0.1-beta.1`
- Sortable (implements sort.Interface)
- database/sql compatible (sql.Scanner/Valuer)
- encoding/json compatible (json.Marshaler/Unmarshaler)
Ranges
------
A `Range` is a set of conditions which specify which versions satisfy the range.
A condition is composed of an operator and a version. The supported operators are:
- `<1.0.0` Less than `1.0.0`
- `<=1.0.0` Less than or equal to `1.0.0`
- `>1.0.0` Greater than `1.0.0`
- `>=1.0.0` Greater than or equal to `1.0.0`
- `1.0.0`, `=1.0.0`, `==1.0.0` Equal to `1.0.0`
- `!1.0.0`, `!=1.0.0` Not equal to `1.0.0`. Excludes version `1.0.0`.
A `Range` can link multiple `Ranges` separated by space:
Ranges can be linked by logical AND:
- `>1.0.0 <2.0.0` would match between both ranges, so `1.1.1` and `1.8.7` but not `1.0.0` or `2.0.0`
- `>1.0.0 <3.0.0 !2.0.3-beta.2` would match every version between `1.0.0` and `3.0.0` except `2.0.3-beta.2`
Ranges can also be linked by logical OR:
- `<2.0.0 || >=3.0.0` would match `1.x.x` and `3.x.x` but not `2.x.x`
AND has a higher precedence than OR. It's not possible to use brackets.
Ranges can be combined by both AND and OR
- `>1.0.0 <2.0.0 || >3.0.0 !4.2.1` would match `1.2.3`, `1.9.9`, `3.1.1`, but not `4.2.1`, `2.1.1`
Range usage:
```
v, err := semver.Parse("1.2.3")
range, err := semver.ParseRange(">1.0.0 <2.0.0 || >=3.0.0")
if range(v) {
//valid
}
```
Example
-----
@ -103,23 +145,30 @@ if err != nil {
}
```
Benchmarks
-----
BenchmarkParseSimple 5000000 328 ns/op 49 B/op 1 allocs/op
BenchmarkParseComplex 1000000 2105 ns/op 263 B/op 7 allocs/op
BenchmarkParseAverage 1000000 1301 ns/op 168 B/op 4 allocs/op
BenchmarkStringSimple 10000000 130 ns/op 5 B/op 1 allocs/op
BenchmarkStringLarger 5000000 280 ns/op 32 B/op 2 allocs/op
BenchmarkStringComplex 3000000 512 ns/op 80 B/op 3 allocs/op
BenchmarkStringAverage 5000000 387 ns/op 47 B/op 2 allocs/op
BenchmarkValidateSimple 500000000 7.92 ns/op 0 B/op 0 allocs/op
BenchmarkValidateComplex 2000000 923 ns/op 0 B/op 0 allocs/op
BenchmarkValidateAverage 5000000 452 ns/op 0 B/op 0 allocs/op
BenchmarkCompareSimple 100000000 11.2 ns/op 0 B/op 0 allocs/op
BenchmarkCompareComplex 50000000 40.9 ns/op 0 B/op 0 allocs/op
BenchmarkCompareAverage 50000000 43.8 ns/op 0 B/op 0 allocs/op
BenchmarkSort 5000000 436 ns/op 259 B/op 2 allocs/op
BenchmarkParseSimple-4 5000000 390 ns/op 48 B/op 1 allocs/op
BenchmarkParseComplex-4 1000000 1813 ns/op 256 B/op 7 allocs/op
BenchmarkParseAverage-4 1000000 1171 ns/op 163 B/op 4 allocs/op
BenchmarkStringSimple-4 20000000 119 ns/op 16 B/op 1 allocs/op
BenchmarkStringLarger-4 10000000 206 ns/op 32 B/op 2 allocs/op
BenchmarkStringComplex-4 5000000 324 ns/op 80 B/op 3 allocs/op
BenchmarkStringAverage-4 5000000 273 ns/op 53 B/op 2 allocs/op
BenchmarkValidateSimple-4 200000000 9.33 ns/op 0 B/op 0 allocs/op
BenchmarkValidateComplex-4 3000000 469 ns/op 0 B/op 0 allocs/op
BenchmarkValidateAverage-4 5000000 256 ns/op 0 B/op 0 allocs/op
BenchmarkCompareSimple-4 100000000 11.8 ns/op 0 B/op 0 allocs/op
BenchmarkCompareComplex-4 50000000 30.8 ns/op 0 B/op 0 allocs/op
BenchmarkCompareAverage-4 30000000 41.5 ns/op 0 B/op 0 allocs/op
BenchmarkSort-4 3000000 419 ns/op 256 B/op 2 allocs/op
BenchmarkRangeParseSimple-4 2000000 850 ns/op 192 B/op 5 allocs/op
BenchmarkRangeParseAverage-4 1000000 1677 ns/op 400 B/op 10 allocs/op
BenchmarkRangeParseComplex-4 300000 5214 ns/op 1440 B/op 30 allocs/op
BenchmarkRangeMatchSimple-4 50000000 25.6 ns/op 0 B/op 0 allocs/op
BenchmarkRangeMatchAverage-4 30000000 56.4 ns/op 0 B/op 0 allocs/op
BenchmarkRangeMatchComplex-4 10000000 153 ns/op 0 B/op 0 allocs/op
See benchmark cases at [semver_test.go](semver_test.go)


+ 224
- 0
range.go View File

@ -0,0 +1,224 @@
package semver
import (
"fmt"
"strings"
"unicode"
)
type comparator func(Version, Version) bool
var (
compEQ comparator = func(v1 Version, v2 Version) bool {
return v1.Compare(v2) == 0
}
compNE = func(v1 Version, v2 Version) bool {
return v1.Compare(v2) != 0
}
compGT = func(v1 Version, v2 Version) bool {
return v1.Compare(v2) == 1
}
compGE = func(v1 Version, v2 Version) bool {
return v1.Compare(v2) >= 0
}
compLT = func(v1 Version, v2 Version) bool {
return v1.Compare(v2) == -1
}
compLE = func(v1 Version, v2 Version) bool {
return v1.Compare(v2) <= 0
}
)
type versionRange struct {
v Version
c comparator
}
// rangeFunc creates a Range from the given versionRange.
func (vr *versionRange) rangeFunc() Range {
return Range(func(v Version) bool {
return vr.c(v, vr.v)
})
}
// Range represents a range of versions.
// A Range can be used to check if a Version satisfies it:
//
// range, err := semver.ParseRange(">1.0.0 <2.0.0")
// range(semver.MustParse("1.1.1") // returns true
type Range func(Version) bool
// OR combines the existing Range with another Range using logical OR.
func (rf Range) OR(f Range) Range {
return Range(func(v Version) bool {
return rf(v) || f(v)
})
}
// AND combines the existing Range with another Range using logical AND.
func (rf Range) AND(f Range) Range {
return Range(func(v Version) bool {
return rf(v) && f(v)
})
}
// ParseRange parses a range and returns a Range.
// If the range could not be parsed an error is returned.
//
// Valid ranges are:
// - "<1.0.0"
// - "<=1.0.0"
// - ">1.0.0"
// - ">=1.0.0"
// - "1.0.0", "=1.0.0", "==1.0.0"
// - "!1.0.0", "!=1.0.0"
//
// A Range can consist of multiple ranges separated by space:
// Ranges can be linked by logical AND:
// - ">1.0.0 <2.0.0" would match between both ranges, so "1.1.1" and "1.8.7" but not "1.0.0" or "2.0.0"
// - ">1.0.0 <3.0.0 !2.0.3-beta.2" would match every version between 1.0.0 and 3.0.0 except 2.0.3-beta.2
//
// Ranges can also be linked by logical OR:
// - "<2.0.0 || >=3.0.0" would match "1.x.x" and "3.x.x" but not "2.x.x"
//
// AND has a higher precedence than OR. It's not possible to use brackets.
//
// Ranges can be combined by both AND and OR
//
// - `>1.0.0 <2.0.0 || >3.0.0 !4.2.1` would match `1.2.3`, `1.9.9`, `3.1.1`, but not `4.2.1`, `2.1.1`
func ParseRange(s string) (Range, error) {
parts := splitAndTrim(s)
orParts, err := splitORParts(parts)
if err != nil {
return nil, err
}
var orFn Range
for _, p := range orParts {
var andFn Range
for _, ap := range p {
opStr, vStr, err := splitComparatorVersion(ap)
if err != nil {
return nil, err
}
vr, err := buildVersionRange(opStr, vStr)
if err != nil {
return nil, fmt.Errorf("Could not parse Range %q: %s", ap, err)
}
rf := vr.rangeFunc()
// Set function
if andFn == nil {
andFn = rf
} else { // Combine with existing function
andFn = andFn.AND(rf)
}
}
if orFn == nil {
orFn = andFn
} else {
orFn = orFn.OR(andFn)
}
}
return orFn, nil
}
// splitORParts splits the already cleaned parts by '||'.
// Checks for invalid positions of the operator and returns an
// error if found.
func splitORParts(parts []string) ([][]string, error) {
var ORparts [][]string
last := 0
for i, p := range parts {
if p == "||" {
if i == 0 {
return nil, fmt.Errorf("First element in range is '||'")
}
ORparts = append(ORparts, parts[last:i])
last = i + 1
}
}
if last == len(parts) {
return nil, fmt.Errorf("Last element in range is '||'")
}
ORparts = append(ORparts, parts[last:])
return ORparts, nil
}
// buildVersionRange takes a slice of 2: operator and version
// and builds a versionRange, otherwise an error.
func buildVersionRange(opStr, vStr string) (*versionRange, error) {
c := parseComparator(opStr)
if c == nil {
return nil, fmt.Errorf("Could not parse comparator %q in %q", opStr, strings.Join([]string{opStr, vStr}, ""))
}
v, err := Parse(vStr)
if err != nil {
return nil, fmt.Errorf("Could not parse version %q in %q: %s", vStr, strings.Join([]string{opStr, vStr}, ""), err)
}
return &versionRange{
v: v,
c: c,
}, nil
}
// splitAndTrim splits a range string by spaces and cleans leading and trailing spaces
func splitAndTrim(s string) (result []string) {
last := 0
for i := 0; i < len(s); i++ {
if s[i] == ' ' {
if last < i-1 {
result = append(result, s[last:i])
}
last = i + 1
}
}
if last < len(s)-1 {
result = append(result, s[last:])
}
// parts := strings.Split(s, " ")
// for _, x := range parts {
// if s := strings.TrimSpace(x); len(s) != 0 {
// result = append(result, s)
// }
// }
return
}
// splitComparatorVersion splits the comparator from the version.
// Spaces between the comparator and the version are not allowed.
// Input must be free of leading or trailing spaces.
func splitComparatorVersion(s string) (string, string, error) {
i := strings.IndexFunc(s, unicode.IsDigit)
if i == -1 {
return "", "", fmt.Errorf("Could not get version from string: %q", s)
}
return strings.TrimSpace(s[0:i]), s[i:], nil
}
func parseComparator(s string) comparator {
switch s {
case "==":
fallthrough
case "":
fallthrough
case "=":
return compEQ
case ">":
return compGT
case ">=":
return compGE
case "<":
return compLT
case "<=":
return compLE
case "!":
fallthrough
case "!=":
return compNE
}
return nil
}

+ 442
- 0
range_test.go View File

@ -0,0 +1,442 @@
package semver
import (
"reflect"
"strings"
"testing"
)
type comparatorTest struct {
input string
comparator func(comparator) bool
}
func TestParseComparator(t *testing.T) {
compatorTests := []comparatorTest{
{">", testGT},
{">=", testGE},
{"<", testLT},
{"<=", testLE},
{"", testEQ},
{"=", testEQ},
{"==", testEQ},
{"!=", testNE},
{"!", testNE},
{"-", nil},
{"<==", nil},
{"<<", nil},
{">>", nil},
}
for _, tc := range compatorTests {
if c := parseComparator(tc.input); c == nil {
if tc.comparator != nil {
t.Errorf("Comparator nil for case %q\n", tc.input)
}
} else if !tc.comparator(c) {
t.Errorf("Invalid comparator for case %q\n", tc.input)
}
}
}
var (
v1 = MustParse("1.2.2")
v2 = MustParse("1.2.3")
v3 = MustParse("1.2.4")
)
func testEQ(f comparator) bool {
return f(v1, v1) && !f(v1, v2)
}
func testNE(f comparator) bool {
return !f(v1, v1) && f(v1, v2)
}
func testGT(f comparator) bool {
return f(v2, v1) && f(v3, v2) && !f(v1, v2) && !f(v1, v1)
}
func testGE(f comparator) bool {
return f(v2, v1) && f(v3, v2) && !f(v1, v2)
}
func testLT(f comparator) bool {
return f(v1, v2) && f(v2, v3) && !f(v2, v1) && !f(v1, v1)
}
func testLE(f comparator) bool {
return f(v1, v2) && f(v2, v3) && !f(v2, v1)
}
func TestSplitAndTrim(t *testing.T) {
tests := []struct {
i string
s []string
}{
{"1.2.3 1.2.3", []string{"1.2.3", "1.2.3"}},
{" 1.2.3 1.2.3 ", []string{"1.2.3", "1.2.3"}}, // Spaces
{"1.2.3 || >=1.2.3 <1.2.3", []string{"1.2.3", "||", ">=1.2.3", "<1.2.3"}},
{" 1.2.3 || >=1.2.3 <1.2.3 ", []string{"1.2.3", "||", ">=1.2.3", "<1.2.3"}},
}
for _, tc := range tests {
p := splitAndTrim(tc.i)
if !reflect.DeepEqual(p, tc.s) {
t.Errorf("Invalid for case %q: Expected %q, got: %q", tc.i, tc.s, p)
}
}
}
func TestSplitComparatorVersion(t *testing.T) {
tests := []struct {
i string
p []string
}{
{">1.2.3", []string{">", "1.2.3"}},
{">=1.2.3", []string{">=", "1.2.3"}},
{"<1.2.3", []string{"<", "1.2.3"}},
{"<=1.2.3", []string{"<=", "1.2.3"}},
{"1.2.3", []string{"", "1.2.3"}},
{"=1.2.3", []string{"=", "1.2.3"}},
{"==1.2.3", []string{"==", "1.2.3"}},
{"!=1.2.3", []string{"!=", "1.2.3"}},
{"!1.2.3", []string{"!", "1.2.3"}},
{"error", nil},
}
for _, tc := range tests {
if op, v, err := splitComparatorVersion(tc.i); err != nil {
if tc.p != nil {
t.Errorf("Invalid for case %q: Expected %q, got error %q", tc.i, tc.p, err)
}
} else if op != tc.p[0] {
t.Errorf("Invalid operator for case %q: Expected %q, got: %q", tc.i, tc.p[0], op)
} else if v != tc.p[1] {
t.Errorf("Invalid version for case %q: Expected %q, got: %q", tc.i, tc.p[1], v)
}
}
}
func TestBuildVersionRange(t *testing.T) {
tests := []struct {
opStr string
vStr string
c func(comparator) bool
v string
}{
{">", "1.2.3", testGT, "1.2.3"},
{">=", "1.2.3", testGE, "1.2.3"},
{"<", "1.2.3", testLT, "1.2.3"},
{"<=", "1.2.3", testLE, "1.2.3"},
{"", "1.2.3", testEQ, "1.2.3"},
{"=", "1.2.3", testEQ, "1.2.3"},
{"==", "1.2.3", testEQ, "1.2.3"},
{"!=", "1.2.3", testNE, "1.2.3"},
{"!", "1.2.3", testNE, "1.2.3"},
{">>", "1.2.3", nil, ""}, // Invalid comparator
{"=", "invalid", nil, ""}, // Invalid version
}
for _, tc := range tests {
if r, err := buildVersionRange(tc.opStr, tc.vStr); err != nil {
if tc.c != nil {
t.Errorf("Invalid for case %q: Expected %q, got error %q", strings.Join([]string{tc.opStr, tc.vStr}, ""), tc.v, err)
}
} else if r == nil {
t.Errorf("Invalid for case %q: got nil", strings.Join([]string{tc.opStr, tc.vStr}, ""))
} else {
// test version
if tv := MustParse(tc.v); !r.v.EQ(tv) {
t.Errorf("Invalid for case %q: Expected version %q, got: %q", strings.Join([]string{tc.opStr, tc.vStr}, ""), tv, r.v)
}
// test comparator
if r.c == nil {
t.Errorf("Invalid for case %q: got nil comparator", strings.Join([]string{tc.opStr, tc.vStr}, ""))
continue
}
if !tc.c(r.c) {
t.Errorf("Invalid comparator for case %q\n", strings.Join([]string{tc.opStr, tc.vStr}, ""))
}
}
}
}
func TestSplitORParts(t *testing.T) {
tests := []struct {
i []string
o [][]string
}{
{[]string{">1.2.3", "||", "<1.2.3", "||", "=1.2.3"}, [][]string{
[]string{">1.2.3"},
[]string{"<1.2.3"},
[]string{"=1.2.3"},
}},
{[]string{">1.2.3", "<1.2.3", "||", "=1.2.3"}, [][]string{
[]string{">1.2.3", "<1.2.3"},
[]string{"=1.2.3"},
}},
{[]string{">1.2.3", "||"}, nil},
{[]string{"||", ">1.2.3"}, nil},
}
for _, tc := range tests {
o, err := splitORParts(tc.i)
if err != nil && tc.o != nil {
t.Errorf("Unexpected error for case %q: %s", tc.i, err)
}
if !reflect.DeepEqual(tc.o, o) {
t.Errorf("Invalid for case %q: Expected %q, got: %q", tc.i, tc.o, o)
}
}
}
func TestVersionRangeToRange(t *testing.T) {
vr := versionRange{
v: MustParse("1.2.3"),
c: compLT,
}
rf := vr.rangeFunc()
if !rf(MustParse("1.2.2")) || rf(MustParse("1.2.3")) {
t.Errorf("Invalid conversion to range func")
}
}
func TestRangeAND(t *testing.T) {
v := MustParse("1.2.2")
v1 := MustParse("1.2.1")
v2 := MustParse("1.2.3")
rf1 := Range(func(v Version) bool {
return v.GT(v1)
})
rf2 := Range(func(v Version) bool {
return v.LT(v2)
})
rf := rf1.AND(rf2)
if rf(v1) {
t.Errorf("Invalid rangefunc, accepted: %s", v1)
}
if rf(v2) {
t.Errorf("Invalid rangefunc, accepted: %s", v2)
}
if !rf(v) {
t.Errorf("Invalid rangefunc, did not accept: %s", v)
}
}
func TestRangeOR(t *testing.T) {
tests := []struct {
v Version
b bool
}{
{MustParse("1.2.0"), true},
{MustParse("1.2.2"), false},
{MustParse("1.2.4"), true},
}
v1 := MustParse("1.2.1")
v2 := MustParse("1.2.3")
rf1 := Range(func(v Version) bool {
return v.LT(v1)
})
rf2 := Range(func(v Version) bool {
return v.GT(v2)
})
rf := rf1.OR(rf2)
for _, tc := range tests {
if r := rf(tc.v); r != tc.b {
t.Errorf("Invalid for case %q: Expected %t, got %t", tc.v, tc.b, r)
}
}
}
func TestParseRange(t *testing.T) {
type tv struct {
v string
b bool
}
tests := []struct {
i string
t []tv
}{
// Simple expressions
{">1.2.3", []tv{
{"1.2.2", false},
{"1.2.3", false},
{"1.2.4", true},
}},
{">=1.2.3", []tv{
{"1.2.3", true},
{"1.2.4", true},
{"1.2.2", false},
}},
{"<1.2.3", []tv{
{"1.2.2", true},
{"1.2.3", false},
{"1.2.4", false},
}},
{"<=1.2.3", []tv{
{"1.2.2", true},
{"1.2.3", true},
{"1.2.4", false},
}},
{"1.2.3", []tv{
{"1.2.2", false},
{"1.2.3", true},
{"1.2.4", false},
}},
{"=1.2.3", []tv{
{"1.2.2", false},
{"1.2.3", true},
{"1.2.4", false},
}},
{"==1.2.3", []tv{
{"1.2.2", false},
{"1.2.3", true},
{"1.2.4", false},
}},
{"!=1.2.3", []tv{
{"1.2.2", true},
{"1.2.3", false},
{"1.2.4", true},
}},
{"!1.2.3", []tv{
{"1.2.2", true},
{"1.2.3", false},
{"1.2.4", true},
}},
// Simple Expression errors
{">>1.2.3", nil},
{"!1.2.3", nil},
{"1.0", nil},
{"string", nil},
{"", nil},
// AND Expressions
{">1.2.2 <1.2.4", []tv{
{"1.2.2", false},
{"1.2.3", true},
{"1.2.4", false},
}},
{"<1.2.2 <1.2.4", []tv{
{"1.2.1", true},
{"1.2.2", false},
{"1.2.3", false},
{"1.2.4", false},
}},
{">1.2.2 <1.2.5 !=1.2.4", []tv{
{"1.2.2", false},
{"1.2.3", true},
{"1.2.4", false},
{"1.2.5", false},
}},
{">1.2.2 <1.2.5 !1.2.4", []tv{
{"1.2.2", false},
{"1.2.3", true},
{"1.2.4", false},
{"1.2.5", false},
}},
// OR Expressions
{">1.2.2 || <1.2.4", []tv{
{"1.2.2", true},
{"1.2.3", true},
{"1.2.4", true},
}},
{"<1.2.2 || >1.2.4", []tv{
{"1.2.2", false},
{"1.2.3", false},
{"1.2.4", false},
}},
// Combined Expressions
{">1.2.2 <1.2.4 || >=2.0.0", []tv{
{"1.2.2", false},
{"1.2.3", true},
{"1.2.4", false},
{"2.0.0", true},
{"2.0.1", true},
}},
{">1.2.2 <1.2.4 || >=2.0.0 <3.0.0", []tv{
{"1.2.2", false},
{"1.2.3", true},
{"1.2.4", false},
{"2.0.0", true},
{"2.0.1", true},
{"2.9.9", true},
{"3.0.0", false},
}},
}
for _, tc := range tests {
r, err := ParseRange(tc.i)
if err != nil && tc.t != nil {
t.Errorf("Error parsing range %q: %s", tc.i, err)
continue
}
for _, tvc := range tc.t {
v := MustParse(tvc.v)
if res := r(v); res != tvc.b {
t.Errorf("Invalid for case %q matching %q: Expected %t, got: %t", tc.i, tvc.v, tvc.b, res)
}
}
}
}
func BenchmarkRangeParseSimple(b *testing.B) {
const VERSION = ">1.0.0"
b.ReportAllocs()
b.ResetTimer()
for n := 0; n < b.N; n++ {
ParseRange(VERSION)
}
}
func BenchmarkRangeParseAverage(b *testing.B) {
const VERSION = ">=1.0.0 <2.0.0"
b.ReportAllocs()
b.ResetTimer()
for n := 0; n < b.N; n++ {
ParseRange(VERSION)
}
}
func BenchmarkRangeParseComplex(b *testing.B) {
const VERSION = ">=1.0.0 <2.0.0 || >=3.0.1 <4.0.0 !=3.0.3 || >=5.0.0"
b.ReportAllocs()
b.ResetTimer()
for n := 0; n < b.N; n++ {
ParseRange(VERSION)
}
}
func BenchmarkRangeMatchSimple(b *testing.B) {
const VERSION = ">1.0.0"
r, _ := ParseRange(VERSION)
v := MustParse("2.0.0")
b.ReportAllocs()
b.ResetTimer()
for n := 0; n < b.N; n++ {
r(v)
}
}
func BenchmarkRangeMatchAverage(b *testing.B) {
const VERSION = ">=1.0.0 <2.0.0"
r, _ := ParseRange(VERSION)
v := MustParse("1.2.3")
b.ReportAllocs()
b.ResetTimer()
for n := 0; n < b.N; n++ {
r(v)
}
}
func BenchmarkRangeMatchComplex(b *testing.B) {
const VERSION = ">=1.0.0 <2.0.0 || >=3.0.1 <4.0.0 !=3.0.3 || >=5.0.0"
r, _ := ParseRange(VERSION)
v := MustParse("5.0.1")
b.ReportAllocs()
b.ResetTimer()
for n := 0; n < b.N; n++ {
r(v)
}
}

Loading…
Cancel
Save