commit b9b57ad93f7448fd5c764955e96bb8e1e29f85fb Author: ge Date: Fri Dec 26 22:38:52 2025 +0300 init diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..01072ca --- /dev/null +++ b/.editorconfig @@ -0,0 +1,8 @@ +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.v] +indent_style = tab diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..9a98968 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,8 @@ +* text=auto eol=lf +*.bat eol=crlf + +*.v linguist-language=V +*.vv linguist-language=V +*.vsh linguist-language=V +v.mod linguist-language=V +.vdocignore linguist-language=ignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..eb03844 --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +# Binaries for programs and plugins +main +ranges +*.exe +*.exe~ +*.so +*.dylib +*.dll + +# Ignore binary output folders +bin/ + +# Ignore common editor/system specific metadata +.DS_Store +.idea/ +.vscode/ +*.iml + +# ENV +.env + +# vweb and database +*.db +*.js diff --git a/README.md b/README.md new file mode 100644 index 0000000..8e86a7b --- /dev/null +++ b/README.md @@ -0,0 +1,20 @@ +# Operating with 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 +end points, a step size, and an inclusive/exclusive flag. + +```v +import ranges + +// Iterate from 0 to 5 with step 2. Negative values also supported. +for i in ranges.range(0, 5, 2) { + println(i) +} +// 0 +// 2 +// 4 +``` + +See more usage examples in [ranges_test.v](ranges_test.v). diff --git a/UNLICENSE b/UNLICENSE new file mode 100644 index 0000000..c91541e --- /dev/null +++ b/UNLICENSE @@ -0,0 +1,22 @@ +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or distribute this +software, either in source code form or as a compiled binary, for any purpose, +commercial or non-commercial, and by any means. + +In jurisdictions that recognize copyright laws, the author or authors of this +software dedicate any and all copyright interest in the software to the public +domain. We make this dedication for the benefit of the public at large and to +the detriment of our heirs and +successors. We intend this dedication to be an overt act of relinquishment in +perpetuity of all present and future rights to this software under copyright +law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF +CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to diff --git a/ranges.v b/ranges.v new file mode 100644 index 0000000..af2a04e --- /dev/null +++ b/ranges.v @@ -0,0 +1,206 @@ +module ranges + +import strconv +import math.big + +struct Range[T] { + limit T + step T + is_neg bool +mut: + cur T +} + +// next returns the new element from range or none if range end is reached. +pub fn (mut r Range[T]) next() ?T { + if (r.is_neg && r.cur < r.limit) || (!r.is_neg && r.cur > r.limit) { + return none + } + defer { + r.cur += r.step + } + return r.cur +} + +@[params] +pub struct RangeConfig { +pub: + // If true exclude the end value from range. + exclusive bool +} + +// range creates new Range iterator with given start, end and step values. +// +// Generally numbers are expected. If type is a struct the following operators +// must be overloaded to perform comparisons and arithmetics: `+`, `-`, `<`, `==`. +// See https://docs.vlang.io/limited-operator-overloading.html for details. +// +// By default, the range includes the end value. This behavior can be changed +// by enabling the 'exclusive' option. +// +// Note: Zero step value will cause an infitite loop! +pub fn range[T](start T, end T, step T, config RangeConfig) Range[T] { + mut limit := end + if config.exclusive { + limit -= step + } + return Range[T]{ + limit: limit + step: step + cur: start + is_neg: start > end + } +} + +@[params] +pub struct RangeFromStringConfig { + RangeConfig +pub: + sep string = '-' + group_sep string = ',' +} + +// from_string parses the input string and returns an array of iterators. +// This function supports only V native number types and big.Integer. +// Use from_string_custom if you want to use custom type with special string +// convertion rules. +// +// Supported string formats are `start-end`, `start[:step]end`. start and end +// values are sepatared by 'sep' which is hypen (`-`) by default. Single number +// will be interpreted as range of one element. Several ranges can be specified +// in a line, separated by 'group_sep' (comma by default). 'sep' and 'group_sep' +// can be overrided by user. +// +// Some example input strings: +// +// * `5` - range from 5 to 5 (single element). +// * `0-10` - range from 0 to 10. +// * `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. +// * `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. +// For all other cases, the step is equal to one. +// +// Example: assert ranges.from_string[int]('1-7')! == [ranges.range(1, 7, 1)] +pub fn from_string[T](s string, config RangeFromStringConfig) ![]Range[T] { + mut result := []Range[T]{} + for i in s.split(config.group_sep) { + range_str := parse_string(i, config.sep)! + // vfmt off + result << range[T]( + convert_string[T](range_str[0])!, + convert_string[T](range_str[1])!, + convert_string[T](range_str[2])!, + config.RangeConfig) + // vfmt on + } + return result +} + +pub type StringConvertFn[T] = fn (s string) !T + +// from_string_custom parses the input string using `conv` function to convert +// string values into numbers and returns an array of iterators. This is an extended +// version of from_string with the same semanthics. +// Example: +// ```v +// import math.big +// import ranges +// +// conv := fn (s string) !big.Integer { +// return big.integer_from_string(s)! +// } +// +// for range in ranges.from_string_custom('0-3,8,11-13', conv)! { +// for i in range { +// println(i) +// } +// } +// // 0 +// // 1 +// // 2 +// // 3 +// // 8 +// // 11 +// // 12 +// // 13 +// ``` +pub fn from_string_custom[T](s string, conv StringConvertFn[T], config RangeFromStringConfig) ![]Range[T] { + mut result := []Range[T]{} + for i in s.split(config.group_sep) { + range_str := parse_string(i, config.sep)! + start := conv[T](range_str[0])! + end := conv[T](range_str[1])! + step := conv[T](range_str[2])! + result << range(start, end, step, config.RangeConfig) + } + return result +} + +fn parse_string(s string, sep string) ![]string { + parts := s.split(sep) + if parts.any(|x| x.is_blank()) || parts.len !in [1, 2, 3] { + return error('`start${sep}end` or `start[:step]:end`' + + "formatted string expected, not '${s}'") + } + if parts.len == 1 { + return [parts[0], parts[0], '1'] + } else if parts.len == 2 { + return [parts[0], parts[1], '1'] + } else if sep == ':' && parts.len == 3 { + return [parts[0], parts[2], parts[1]] + } + return error('invalid range string: ${s}') +} + +fn convert_string[T](s string) !T { + $match T { + int { + return strconv.atoi(s)! + } + i8 { + return strconv.atoi8(s)! + } + i16 { + return strconv.atoi16(s)! + } + i32 { + return strconv.atoi32(s)! + } + i64 { + return strconv.atoi64(s)! + } + isize { + return isize(strconv.atoi64(s)!) + } + u8 { + return strconv.atou8(s)! + } + u16 { + return strconv.atou16(s)! + } + u32 { + return strconv.atou32(s)! + } + u64 { + return strconv.atou64(s)! + } + usize { + return usize(strconv.atou64(s)!) + } + f32 { + return f32(strconv.atof64(s)!) + } + f64 { + return strconv.atof64(s)! + } + big.Integer { + return big.integer_from_string(s)! + } + $else { + return error("cannot convert '${s}' to ${typeof[T]().name}") + } + } + return error('unexpected string convert error') +} diff --git a/ranges_test.v b/ranges_test.v new file mode 100644 index 0000000..03ccc58 --- /dev/null +++ b/ranges_test.v @@ -0,0 +1,171 @@ +import ranges +import math.big + +fn test_range() { + mut result := []int{} + for i in ranges.range[int](0, 10, 1) { + result << i + } + 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() { + mut result := []int{} + for i in ranges.range[int](10, 0, -1) { + result << i + } + 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() { + mut result := []int{} + for i in ranges.range[int](0, 10, 2) { + result << i + } + assert result == [0, 2, 4, 6, 8, 10] +} + +fn test_range_with_negative_step() { + mut result := []int{} + for i in ranges.range[int](5, 0, -1) { + result << i + } + assert result == [5, 4, 3, 2, 1, 0] +} + +fn test_range_with_non_odd_step() { + mut result := []int{} + for i in ranges.range[int](0, 5, 2) { + result << i + } + assert result == [0, 2, 4] +} + +fn test_range_single_item() { + mut result := []int{} + for i in ranges.range(0, 0, 1) { + result << i + } + 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() { + start := big.zero_int + end := big.integer_from_int(5) + step := big.one_int + mut result := []big.Integer{} + for i in ranges.range[big.Integer](start, end, step) { + result << i + } + assert result == [ + big.integer_from_int(0), + big.integer_from_int(1), + big.integer_from_int(2), + big.integer_from_int(3), + big.integer_from_int(4), + big.integer_from_int(5), + ] +} + +fn test_range_from_string() { + 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), + 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[int]('100:-1:0', sep: ':')! == [ + ranges.range(100, 0, -1), + ] + assert ranges.from_string[int]('1..10', sep: '..')! == [ranges.range(1, 10, 1)] + assert ranges.from_string[int]('-256..256', sep: '..')! == [ + ranges.range(-256, 256, 1), + ] + assert ranges.from_string[int]('256..-256', sep: '..')! == [ + ranges.range(256, -256, 1), + ] + assert ranges.from_string[f32]('0.0..99.99', sep: '..')! == [ + ranges.range[f32](0.0, 99.99, 1), + ] +} + +struct Int { + val int +} + +fn (a Int) + (b Int) Int { + return Int{a.val + b.val} +} + +fn (a Int) - (b Int) Int { + return Int{a.val - b.val} +} + +fn (a Int) < (b Int) bool { + return a.val < b.val +} + +fn (a Int) == (b Int) bool { + return a.val == b.val +} + +fn test_range_custom_type() { + // vfmt off + mut result := []Int{} + for i in ranges.range[Int](Int{ val: 0 }, Int{ val: 5 }, Int{ val: 1 }) { + result << i + } + assert result == [Int{0}, Int{1}, Int{2}, Int{3}, Int{4}, Int{5}] + // vfmt on +} + +// +// Note this bug: https://github.com/vlang/v/issues/26156 +// + +fn test_range_from_string_custom_type() { + assert ranges.from_string_custom[Int]('0-5', fn (s string) !Int { + if s.is_int() { + return Int{ val: s.int() } + } else { + return error('invalid integer value: ${s}') + } + })! == [ranges.range[Int](Int{0}, Int{5}, Int{1})] + + convert_fn := fn (s string) !Int { + if s.is_int() { + return Int{ + val: s.int() + } + } else { + return error('invalid integer value: ${s}') + } + } + assert ranges.from_string_custom[Int]('0..10', convert_fn, + sep: '..' + )! == [ranges.range[Int](Int{0}, Int{10}, Int{1})] +} diff --git a/v.mod b/v.mod new file mode 100644 index 0000000..5e75b7e --- /dev/null +++ b/v.mod @@ -0,0 +1,7 @@ +Module { + name: 'ranges' + description: 'Operating with ranges of numbers' + version: '0.0.0' + license: 'Unlicense' + dependencies: [] +}