14 Commits
Author SHA1 Message Date
ge d2c5ad5d2f version 0.6.0
* Check for zero step value to void infinite loops
* Add `new()` fn for creating ranges with checks
* Add `to_array()` method
* Add `is_empty()` method
* Add more tests
2026-07-15 01:09:33 +03:00
ge 02d06423b7 tests: Disable -no-skip-unused flag on tests, since https://github.com/vlang/v/issues/27147 is closed 2026-05-31 22:29:38 +03:00
ge 34ed972930 fix tests 2026-05-12 19:55:19 +03:00
ge a055360436 mod: bump version 2026-03-24 21:44:20 +03:00
ge 93a610fa02 feat: add bounds(), make Range private back 2026-03-24 21:43:46 +03:00
ge fe67a0c5a6 mod: bump version 2026-03-24 21:05:50 +03:00
ge 808870ff09 feat: add start-end[/step] syntax support, make Range public @[noinit] 2026-03-24 21:04:32 +03:00
ge 39314b474b use struct update syntax in with_step() 2026-01-11 08:41:46 +03:00
ge 06f85c48d3 mod: bump version 2026-01-11 03:13:54 +03:00
ge 74e92c77ab feat: add with_step(), reset() 2026-01-11 03:13:01 +03:00
ge b51f74a877 readme: fix 2025-12-30 15:43:51 +03:00
ge b1eaeee6f0 readme: fix Range description 2025-12-30 12:30:04 +03:00
ge 899dacba7a mod: bump version 2025-12-30 12:27:08 +03:00
ge 4306b2220c breaking,fix: remove range exclusivity support
The usefulness of this feature is questionable, and its correct implementation
is difficult due to the use of generics. The implementation worked incorrectly
in cases where the iterator step is not equal to one and is not a multiple of
the end element. For example, for `range(0, 7, 4)`, the result is `[0]`
instead of `[0, 4]`. After this commit range end value is always included.

To check for multiples, a user-defined type must also implement an overload of
the remainder operator (`%`), even if the exclusivity function is not needed.

Another correct implementation requires subtracting one from the end element
for integers and the minimum fractional part supported by the type for floats.
This cannot be done correctly for generics, as it requires casting the literal
to a specific type, and we cannot cast number to any type.
2025-12-30 12:19:35 +03:00
4 changed files with 275 additions and 72 deletions
+1 -1
View File
@@ -3,7 +3,7 @@
The `ranges` module provides tools for creating ranges of numbers. The `ranges` module provides tools for creating ranges of numbers.
Ranges are represented by the generic `Range` iterator, which has start and Ranges are represented by the generic `Range` iterator, which has start and
end points, a step size, and an inclusive/exclusive flag. end points and a step size.
```v ```v
import ranges import ranges
+99 -33
View File
@@ -4,16 +4,20 @@ import strconv
import math.big import math.big
struct Range[T] { struct Range[T] {
limit T start T
end T
step T step T
is_neg bool is_rev bool // Is set to true if range end value is lesser than start value.
mut: mut:
cur T cur T
} }
// next returns the new element from range or none if range end is reached. // next returns the new element from range or none if range end is reached.
pub fn (mut r Range[T]) next() ?T { pub fn (mut r Range[T]) next() ?T {
if (r.is_neg && r.cur < r.limit) || (!r.is_neg && r.cur > r.limit) { if r.step == $zero(T) {
return none
}
if (r.is_rev && r.cur < r.end) || (!r.is_rev && r.cur > r.end) {
return none return none
} }
defer { defer {
@@ -22,11 +26,51 @@ pub fn (mut r Range[T]) next() ?T {
return r.cur return r.cur
} }
@[params] // reset resets the internal iterator state to its initial value, after which the iterator can be reused.
pub struct RangeConfig { // Note: `for i in iter {` does not modify the internal iterator state, but direct `next()` call does.
pub: pub fn (mut r Range[T]) reset() {
// If true exclude the end value from range. r.cur = r.start
exclusive bool }
// bounds returns the start, end and step values of range.
pub fn (r Range[T]) bounds() (T, T, T) {
return r.start, r.end, r.step
}
// with_step returns copy of the range with new step value.
pub fn (r Range[T]) with_step[T](step T) Range[T] {
return Range[T]{
...r
step: step
cur: r.start
}
}
// to_array returns an array of elements from the range.
pub fn (r Range[T]) to_array() []T {
if r.is_empty() {
return []T{}
}
mut cap := 0
$if T is $int || T is $float {
cap = int((r.end - r.start) / r.step + T(1))
} $else $if T is big.Integer {
cap = ((r.end - r.start) / r.step + big.one_int).int()
}
mut arr := []T{cap: cap}
for el in r {
arr << el
}
return arr
}
// is_empty reports is the range instance empty (has no values).
pub fn (r Range[T]) is_empty() bool {
empty := $zero(Range[T])
return r == empty
} }
// range creates new Range iterator with given start, end and step values. // range creates new Range iterator with given start, end and step values.
@@ -35,26 +79,41 @@ pub:
// must be overloaded to perform comparisons and arithmetics: `+`, `-`, `<`, `==`. // must be overloaded to perform comparisons and arithmetics: `+`, `-`, `<`, `==`.
// See https://docs.vlang.io/limited-operator-overloading.html for details. // See https://docs.vlang.io/limited-operator-overloading.html for details.
// //
// By default, the range includes the end value. This behavior can be changed // The range includes the end value.
// by enabling the 'exclusive' option.
// //
// Note: Zero step value will cause an infitite loop! // Note: If range cannot be created the empty range will be returned. See also `new()`.
pub fn range[T](start T, end T, step T, config RangeConfig) Range[T] { pub fn range[T](start T, end T, step T) Range[T] {
mut limit := end return new(start, end, step) or { Range[T]{} }
if config.exclusive { }
limit -= step
// new creates new range with given start, end and step values.
// Unlike `range`, this function will return an error if:
//
// * step value is zero;
// * step > 0, but start value is greather than end value;
// * step < 0, but start value is lesser than end value.
pub fn new[T](start T, end T, step T) !Range[T] {
zero := $zero(T)
if step == zero {
return error('step value is zero')
}
if step > zero && start > end {
return error('step is positive, but start value is greather than end value')
}
if step < zero && start <= end {
return error('step is negative, but start value is lesser than or equals end value')
} }
return Range[T]{ return Range[T]{
limit: limit start: start
end: end
step: step step: step
cur: start cur: start
is_neg: start > end is_rev: start > end
} }
} }
@[params] @[params]
pub struct RangeFromStringConfig { pub struct RangeFromStringConfig {
RangeConfig
pub: pub:
sep string = '-' sep string = '-'
group_sep string = ',' group_sep string = ','
@@ -65,34 +124,34 @@ pub:
// Use from_string_custom if you want to use custom type with special string // Use from_string_custom if you want to use custom type with special string
// convertion rules. // convertion rules.
// //
// Supported string formats are `start-end`, `start[:step]end`. start and end // Supported string formats are `start-end[/step]` and `start[:step]:end`. start
// values are sepatared by 'sep' which is hypen (`-`) by default. Single number // and end values are sepatared by 'sep' which is hypen (`-`) by default. Single
// will be interpreted as range of one element. Several ranges can be specified // number will be interpreted as range of one element. Several ranges can be
// in a line, separated by 'group_sep' (comma by default). 'sep' and 'group_sep' // specified in a line, separated by 'group_sep' (comma by default). 'sep' and
// can be overrided by user. // 'group_sep' can be overrided by user.
// //
// Some example input strings: // Some example input strings:
// //
// * `5` - range from 5 to 5 (single element). // * `5` - range from 5 to 5 (single element).
// * `0-10` - range from 0 to 10. // * `0-10` - range from 0 to 10.
// * `0-100/5` - range in Cron-style syntax from 0 to 100 with step 5.
// * `15:-1:0` - range in MathLab-style syntax from 15 to 0 with negative step -1. // * `15:-1:0` - range in MathLab-style syntax from 15 to 0 with negative step -1.
// * `1..8` - range from 1 to 8 with '..' sep. // * `1..8` - range from 1 to 8 with '..' sep.
// * `0-7,64-71` - multiple ranges: from 0 to 7 and from 64 to 71. // * `0-7,64-71` - multiple ranges: from 0 to 7 and from 64 to 71.
// //
// Only MathLab-style syntax allows you to specify a step directly in the string. // If the step value is not specified, it will be set to one.
// For all other cases, the step is equal to one.
// //
// Example: assert ranges.from_string[int]('1-7')! == [ranges.range(1, 7, 1)] // Example: assert ranges.from_string[int]('1-7')! == [ranges.range(1, 7, 1)]
pub fn from_string[T](s string, config RangeFromStringConfig) ![]Range[T] { pub fn from_string[T](s string, config RangeFromStringConfig) ![]Range[T] {
mut result := []Range[T]{} mut result := []Range[T]{}
for i in s.split(config.group_sep) { for i in s.split(config.group_sep) {
range_str := parse_string(i, config.sep)! range_str := split_string(i, config.sep)!
// vfmt off // vfmt off
result << range[T]( result << range[T](
convert_string[T](range_str[0])!, convert_string[T](range_str[0])!,
convert_string[T](range_str[1])!, convert_string[T](range_str[1])!,
convert_string[T](range_str[2])!, convert_string[T](range_str[2])!,
config.RangeConfig) )
// vfmt on // vfmt on
} }
return result return result
@@ -129,16 +188,16 @@ pub type StringConvertFn[T] = fn (s string) !T
pub fn from_string_custom[T](s string, conv StringConvertFn[T], config RangeFromStringConfig) ![]Range[T] { pub fn from_string_custom[T](s string, conv StringConvertFn[T], config RangeFromStringConfig) ![]Range[T] {
mut result := []Range[T]{} mut result := []Range[T]{}
for i in s.split(config.group_sep) { for i in s.split(config.group_sep) {
range_str := parse_string(i, config.sep)! range_str := split_string(i, config.sep)!
start := conv[T](range_str[0])! start := conv[T](range_str[0])!
end := conv[T](range_str[1])! end := conv[T](range_str[1])!
step := conv[T](range_str[2])! step := conv[T](range_str[2])!
result << range(start, end, step, config.RangeConfig) result << range(start, end, step)
} }
return result return result
} }
fn parse_string(s string, sep string) ![]string { fn split_string(s string, sep string) ![]string {
parts := s.split(sep) parts := s.split(sep)
if parts.any(|x| x.is_blank()) || parts.len !in [1, 2, 3] { if parts.any(|x| x.is_blank()) || parts.len !in [1, 2, 3] {
return error('`start${sep}end` or `start[:step]:end`' + return error('`start${sep}end` or `start[:step]:end`' +
@@ -146,12 +205,19 @@ fn parse_string(s string, sep string) ![]string {
} }
if parts.len == 1 { if parts.len == 1 {
return [parts[0], parts[0], '1'] return [parts[0], parts[0], '1']
} else if parts.len == 2 { }
if parts.len == 2 {
if parts[1].contains('/') {
end, step := parts[1].split_once('/') or { '', '' }
return [parts[0], end, step]
}
return [parts[0], parts[1], '1'] return [parts[0], parts[1], '1']
} else if sep == ':' && parts.len == 3 { }
if sep == ':' && parts.len == 3 {
return [parts[0], parts[2], parts[1]] return [parts[0], parts[2], parts[1]]
} }
return error('invalid range string: ${s}') return error('invalid range string: expected `start[${sep}step]${sep}end` ' +
'or `start${sep}end[/step]` format, got `${s}`')
} }
fn convert_string[T](s string) !T { fn convert_string[T](s string) !T {
+174 -37
View File
@@ -9,14 +9,6 @@ fn test_range() {
assert result == [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] assert result == [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
} }
fn test_range_exclusive() {
mut result := []int{}
for i in ranges.range[int](0, 10, 1, exclusive: true) {
result << i
}
assert result == [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
}
fn test_range_negative() { fn test_range_negative() {
mut result := []int{} mut result := []int{}
for i in ranges.range[int](10, 0, -1) { for i in ranges.range[int](10, 0, -1) {
@@ -25,14 +17,6 @@ fn test_range_negative() {
assert result == [10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0] assert result == [10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
} }
fn test_range_negative_exclusive() {
mut result := []int{}
for i in ranges.range[int](10, 0, -1, exclusive: true) {
result << i
}
assert result == [10, 9, 8, 7, 6, 5, 4, 3, 2, 1]
}
fn test_range_with_step() { fn test_range_with_step() {
mut result := []int{} mut result := []int{}
for i in ranges.range[int](0, 10, 2) { for i in ranges.range[int](0, 10, 2) {
@@ -65,14 +49,6 @@ fn test_range_single_item() {
assert result == [0] assert result == [0]
} }
fn test_range_single_item_exclusive() {
mut result := []int{}
for i in ranges.range(0, 1, 1, exclusive: true) {
result << i
}
assert result == [0]
}
fn test_range_bigint() { fn test_range_bigint() {
start := big.zero_int start := big.zero_int
end := big.integer_from_int(5) end := big.integer_from_int(5)
@@ -93,10 +69,15 @@ fn test_range_bigint() {
fn test_range_from_string() { fn test_range_from_string() {
assert ranges.from_string[int]('0-10')! == [ranges.range(0, 10, 1)] assert ranges.from_string[int]('0-10')! == [ranges.range(0, 10, 1)]
assert ranges.from_string[int]('0-7,8-15')! == [ranges.range(0, 7, 1), assert ranges.from_string[int]('0-7,8-15')! == [
ranges.range(8, 15, 1)] ranges.range(0, 7, 1),
assert ranges.from_string[int]('0-6,7,8-15')! == [ranges.range(0, 6, 1), ranges.range(8, 15, 1),
ranges.range(7, 7, 1), ranges.range(8, 15, 1)] ]
assert ranges.from_string[int]('0-6,7,8-15')! == [
ranges.range(0, 6, 1),
ranges.range(7, 7, 1),
ranges.range(8, 15, 1),
]
assert ranges.from_string[i64]('5:2:15', sep: ':')! == [ranges.range[i64](5, 15, 2)] assert ranges.from_string[i64]('5:2:15', sep: ':')! == [ranges.range[i64](5, 15, 2)]
assert ranges.from_string[int]('100:-1:0', sep: ':')! == [ assert ranges.from_string[int]('100:-1:0', sep: ':')! == [
ranges.range(100, 0, -1), ranges.range(100, 0, -1),
@@ -111,6 +92,7 @@ fn test_range_from_string() {
assert ranges.from_string[f32]('0.0..99.99', sep: '..')! == [ assert ranges.from_string[f32]('0.0..99.99', sep: '..')! == [
ranges.range[f32](0.0, 99.99, 1), ranges.range[f32](0.0, 99.99, 1),
] ]
assert ranges.from_string[int]('0-100/5')! == [ranges.range(0, 100, 5)]
} }
struct Int { struct Int {
@@ -134,23 +116,29 @@ fn (a Int) == (b Int) bool {
} }
fn test_range_custom_type() { fn test_range_custom_type() {
// vfmt off
mut result := []Int{} mut result := []Int{}
for i in ranges.range[Int](Int{ val: 0 }, Int{ val: 5 }, Int{ val: 1 }) { start := Int{0}
end := Int{5}
step := Int{1}
for i in ranges.range[Int](start, end, step) {
result << i result << i
} }
assert result == [Int{0}, Int{1}, Int{2}, Int{3}, Int{4}, Int{5}] assert result == [
// vfmt on Int{0},
Int{1},
Int{2},
Int{3},
Int{4},
Int{5},
]
} }
//
// Note this bug: https://github.com/vlang/v/issues/26156
//
fn test_range_from_string_custom_type() { fn test_range_from_string_custom_type() {
assert ranges.from_string_custom[Int]('0-5', fn (s string) !Int { assert ranges.from_string_custom[Int]('0-5', fn (s string) !Int {
if s.is_int() { if s.is_int() {
return Int{ val: s.int() } return Int{
val: s.int()
}
} else { } else {
return error('invalid integer value: ${s}') return error('invalid integer value: ${s}')
} }
@@ -169,3 +157,152 @@ fn test_range_from_string_custom_type() {
sep: '..' sep: '..'
)! == [ranges.range[Int](Int{0}, Int{10}, Int{1})] )! == [ranges.range[Int](Int{0}, Int{10}, Int{1})]
} }
fn test_range_reset() {
mut result := []int{}
mut iter := ranges.range(0, 5, 1)
for {
if elem := iter.next() {
result << elem
} else {
break
}
}
assert result == [0, 1, 2, 3, 4, 5]
iter.reset()
result = []int{}
for i in iter {
result << i
}
assert result == [0, 1, 2, 3, 4, 5]
}
fn test_range_new_with_step() {
mut result := []int{}
mut iter := ranges.range(0, 5, 1)
for i in iter.with_step(2) {
result << i
}
assert result == [0, 2, 4]
}
fn test_range_bounds() {
r := ranges.range(0, 10, 2)
a, b, c := r.bounds()
assert a == 0
assert b == 10
assert c == 2
}
fn test_range_to_array_int() {
r := ranges.range(0, 5, 1)
assert r.to_array() == [0, 1, 2, 3, 4, 5]
}
fn test_range_to_array_float() {
r := ranges.range[f64](3.0, 3.14, 0.01)
assert r.to_array() == [
f64(3.0),
3.01,
3.0199999999999996,
3.0299999999999994,
3.039999999999999,
3.049999999999999,
3.0599999999999987,
3.0699999999999985,
3.0799999999999983,
3.089999999999998,
3.099999999999998,
3.1099999999999977,
3.1199999999999974,
3.1299999999999972,
3.139999999999997,
]
}
fn test_range_to_array_bigint() {
r := ranges.range(big.zero_int, big.three_int, big.one_int)
assert r.to_array() == [big.zero_int, big.one_int, big.two_int, big.three_int]
}
fn test_range_empty() {
r := ranges.range(0, 0, 0)
assert r.to_array() == []
}
fn test_range_empty_bigint() {
r := ranges.range(big.zero_int, big.zero_int, big.zero_int)
assert r.to_array() == []
}
fn test_range_is_empty() {
r := ranges.range(0, 0, -9000)
assert r.is_empty()
assert r.to_array() == []
}
fn test_new() {
r := ranges.new(0, 10, 1)!
a, b, c := r.bounds()
assert a == 0
assert b == 10
assert c == 1
assert r.to_array() == [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
}
fn test_new_with_step() {
r := ranges.new(0, 10, 2)!
assert r.to_array() == [0, 2, 4, 6, 8, 10]
}
fn test_new_reversed() {
r := ranges.new(10, 0, -1)!
assert r.to_array() == [10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
}
fn test_new_with_negative_step() {
r := ranges.new(10, 0, -2)!
assert r.to_array() == [10, 8, 6, 4, 2, 0]
}
fn test_new_single_item() {
r := ranges.new(5, 5, 1)!
assert r.to_array() == [5]
}
fn test_new_error_zero_step() {
if _ := ranges.new(0, 10, 0) {
assert false, 'expected error for zero step'
} else {
assert err.msg() == 'step value is zero'
}
}
fn test_new_error_positive_step_start_gt_end() {
if _ := ranges.new(10, 0, 1) {
assert false, 'expected error for positive step with start > end'
} else {
assert err.msg() == 'step is positive, but start value is greather than end value'
}
}
fn test_new_error_negative_step_start_lt_end() {
if _ := ranges.new(0, 10, -1) {
assert false, 'expected error for negative step with start <= end'
} else {
assert err.msg() == 'step is negative, but start value is lesser than or equals end value'
}
}
fn test_new_error_negative_step_start_eq_end() {
if _ := ranges.new(5, 5, -1) {
assert false, 'expected error for negative step with start <= end'
} else {
assert err.msg() == 'step is negative, but start value is lesser than or equals end value'
}
}
+1 -1
View File
@@ -1,7 +1,7 @@
Module { Module {
name: 'ranges' name: 'ranges'
description: 'Operating with ranges of numbers' description: 'Operating with ranges of numbers'
version: '0.1.0' version: '0.6.0'
license: 'Unlicense' license: 'Unlicense'
dependencies: [] dependencies: []
} }