├── .github └── workflows │ └── gen-docs.yml ├── .gitignore ├── LICENSE ├── README.md ├── src └── strides.nim ├── strides.nimble └── tests ├── config.nims └── t_strides.nim /.github/workflows/gen-docs.yml: -------------------------------------------------------------------------------- 1 | 2 | name: Test and generate docs 3 | 4 | on: 5 | push: 6 | branches: [ main ] 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Get Date 14 | id: get-date 15 | run: echo "::set-output name=date::$(date '+%Y-%m-%d')" 16 | shell: bash 17 | 18 | - name: Cache choosenim 19 | id: cache-choosenim 20 | uses: actions/cache@v3 21 | with: 22 | path: ~/.choosenim 23 | key: ${{ runner.os }}-choosenim-devel-${{ steps.get-date.outputs.date }} 24 | - name: Cache nimble 25 | id: cache-nimble 26 | uses: actions/cache@v1 27 | with: 28 | path: ~/.nimble 29 | key: ${{ runner.os }}-nimble-${{ hashFiles('strides.nimble') }} 30 | - uses: jiro4989/setup-nim-action@v1 31 | with: 32 | nim-version: 'devel' 33 | 34 | - uses: actions/checkout@v3 35 | 36 | - name: Run tests 37 | run: nimble test 38 | 39 | - name: Clean up old documentation 40 | run: rm -rf ./htmldocs 41 | - name: Generate documentation 42 | run: | 43 | nimble develop -y 44 | rm -rf ./htmldocs 45 | nim doc --project --index:on --git.url:https://github.com/fsh/strides --git.commit:main --git.devel:main --outdir:htmldocs src/strides.nim 46 | - name: Publish documentation to GitHub Pages 47 | uses: peaceiris/actions-gh-pages@v3 48 | with: 49 | github_token: ${{ secrets.GITHUB_TOKEN }} 50 | publish_dir: ./htmldocs 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea 3 | *.log 4 | tmp/ 5 | htmldocs/ 6 | /tests/t_strides 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Frank S. Hestvik 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Strides 3 | 4 | See [generated documentation](https://fsh.github.io/strides/strides.html) for 5 | the most up-to-date information. 6 | 7 | ## Indexing and Slicing with Stride 8 | 9 | One thing missing from default Nim is the ability to do strided slicing. 10 | 11 | This is the ability to specify a range with a _step_ parameter to select every 12 | *n*th element in the range, or to reverse the "direction" of the range in case 13 | of a negative step. 14 | 15 | This package adds the `@:` operator to do just this: 16 | 17 | ``` nim 18 | 19 | import pkg/strides 20 | 21 | let text = "hello world" 22 | 23 | # Regular slice 24 | assert text[ 2 .. ^3 ] == "llo wor" 25 | 26 | # Strided slice with a step of 2. 27 | assert text[ 2 .. ^3 @: 2 ] == "lowr" 28 | 29 | # Strided slice with reversed direction. 30 | assert text[ ^3 .. 2 @: -1 ] == "row oll" 31 | 32 | # Just the stride, like `xs[::s]` in Python: 33 | assert text[ @: -1 ] == "dlrow olleh" 34 | assert text[ @: 2 ] == "hlowrd" 35 | 36 | # Additionally, a third form of length+stride is supported: 37 | 38 | # Positive stride works like `xs[:a:s]` in Python: 39 | assert text[ 10 @: 1 ] == "hello worl" 40 | assert text[ 10 @: 2 ] == "hlowr" 41 | assert text[ 10 @: 3 ] == "hlwl" 42 | 43 | # Negative stride works like `xs[a::s]` in Python: 44 | assert text[ 10 @: -1 ] == "lrow olleh" 45 | assert text[ 10 @: -2 ] == "lo le" 46 | assert text[ 10 @: -3 ] == "lwlh" 47 | ``` 48 | 49 | These strides can also be used as iterators in lieu of `countdown()` and `countup()` if 50 | they do not contain any `BackwardsIndex`es. 51 | 52 | ``` nim 53 | 54 | let k1 = collect: 55 | for i in 0 ..< 10 @: 2: 56 | i 57 | 58 | assert k1 == @[0, 2, 4, 6, 8] 59 | 60 | let k2 = collect: 61 | for i in 20 .. -1 @: -7: 62 | i 63 | 64 | assert k2 == @[20, 13, 6, -1] 65 | ``` 66 | 67 | Here's how they relate: 68 | 69 | | Nim iterator | Python `range()` | Python index | Nim + Strides | 70 | |:---------------------|:--------------------|:---------------|----------------| 71 | | `countdown(a, b, s)` | `range(a, b-1, -s)` | `xs[a:b-1:-s]` | `a .. b @: -s` | 72 | | `countup(a, b, s)` | `range(a, b+1, s)` | `xs[a:b+1:s]` | `a .. b @: s` | 73 | 74 | Note that Nim convention is to be end-point inclusive[^1]. 75 | 76 | [^1]: Unfortunately. 77 | 78 | 79 | ## LinearSegment 80 | 81 | There's more to this package than just `@:`. 82 | 83 | The _resolved_ type of `StridedSlice` (made with `@:`) is a `LinearSegment`. 84 | _Resolved_ here means when any `BackwardsIndex` or `StrideIndex` has been 85 | translated into actual integers by interpreting them in the context of a length. And a `LinearSegment` is the finite version of a `LinearSequence`. 86 | 87 | Check out the [generated documentation](https://fsh.github.io/strides/strides.html) for more. 88 | -------------------------------------------------------------------------------- /src/strides.nim: -------------------------------------------------------------------------------- 1 | ## ======= 2 | ## Strides 3 | ## ======= 4 | ## 5 | ## This module features several types and functionality related to linear sequences 6 | ## and linear indexing. 7 | ## 8 | ## The most useful from an ergonomics perspective is probably `StridedSlice`_ 9 | ## which is like a `HSlice` but with a stride (step). This is akin to Python's 10 | ## general slicing like `xs[start:stop:step]`, or its `range(start,stop,step)` function. 11 | ## 12 | ## The primary way to construct strided slices is with the `@:`_ operator, which 13 | ## can be applied in several ways. 14 | 15 | runnableExamples: 16 | 17 | let greet = "hello world" 18 | 19 | # The obvious way, as a strided slice: 20 | assert greet[ 2 .. 8 @: 3 ] == "l r" 21 | 22 | # As a prefix: assumes the whole length. 23 | assert greet[ @: 2 ] == "hlowrd" 24 | assert greet[ @: -1 ] == "dlrow olleh" 25 | 26 | # As a length + step: shorthand for `0 ..< length` or `length-1 .. 0` 27 | assert greet[ 5 @: 1 ] == "hello" 28 | assert greet[ 5 @: 2 ] == "hlo" 29 | assert greet[ 5 @: -1 ] == "olleh" 30 | 31 | assert greet[greet.len @: 3] == greet[@:3] 32 | 33 | ## 34 | ## .. note:: Negative strides are allowed, so strided slices can actually run backwards. 35 | ## 36 | ## Given a length (for example of the container being sliced) we can "resolve" a 37 | ## `StridedSlice`_ into a `LinearSegment`_ (a finite `LinearSequence`_). A 38 | ## `LinearSegment_` consists of the fields `(initial, stride, count)` which 39 | ## should all be integers. 40 | ## 41 | ## There are two ways to resolve a slice: the Nim way (hard slices) and the Python way (soft slices). 42 | ## 43 | ## Standard Nim behavior is for slices to go out of bounds. For example 44 | ## `"test"[0 ..< 20]` will trigger an exception. Whereas in Python 45 | ## `"test"[0:20]` will work and simply return `"test"`. 46 | ## 47 | ## The first behavior is met with the `^!`_ operator which functions very 48 | ## similar to `^^`. It merely translates `BackwardsIndex` and `StridedIndex` 49 | ## into integers, not doing any bounds checking. 50 | ## 51 | ## The second way is with the operator `^?`_ which does soft slicing. It works 52 | ## the same way as `^!`_ but automatically constrains the slice so it doesn't go 53 | ## out of bounds. 54 | ## 55 | ## By default, and for convenience(?), containers use `^?`_ to mimic Python 56 | ## behavior when using strided slices specifically (might change?), but regular 57 | ## slices (`HSlice`) still triggers out of bounds. 58 | ## 59 | 60 | runnableExamples: 61 | 62 | # ^! resolves the strided slice into a linear segment given a length. 63 | let linseg1 = (1 .. 100 @: 5) ^! 10 64 | assert linseg1 is LinearSegment 65 | assert linseg1.len == 20 66 | 67 | # ^? does the same but constrains the linear segment to `0 ..< length`. 68 | let linseg2 = (1 .. 100 @: 5) ^? 10 69 | assert linseg2.len == 2 # constrained to only cover [1, 6] 70 | 71 | let text = "aAbBcCdDeEfF" 72 | 73 | # "Hard" slices trigger out of bounds: 74 | # text[ (0 .. 100 @: 2) ^! 12 ] --> exception! 75 | 76 | # But this works: 77 | assert text[ (0 .. 100 @: 2) ^? 12 ] == "abcdef" 78 | 79 | ## 80 | ## To use `StridedSlice`_ in a custom container, you would do something like the 81 | ## following: 82 | ## 83 | 84 | runnableExamples: 85 | type MyList = object 86 | 87 | proc len*(_: MyList): int = 69 88 | 89 | proc `[]`*(lst: MyList, idx: AnyStrided): auto = 90 | when idx isnot LinearSegment: 91 | let idx = idx ^? lst.len # or `^!` for slices that trigger out of bounds. 92 | 93 | assert idx is LinearSegment 94 | 95 | # ... 96 | 97 | import std/[math, strformat, algorithm] 98 | 99 | func `&`*(a: Slice, b: distinct Slice): auto = 100 | ## Returns the intersection of two `HSlice`s. 101 | max(a.a, b.a) .. min(a.b, b.b) 102 | 103 | func `$`*(a: BackwardsIndex): string = 104 | ## Missing from standard library? 105 | &"^{a.int}" 106 | 107 | # We want the types to be lightweight so we don't want any nonsense runtime type 108 | # info. We want the segment to compose the sequence, allowing functions of the 109 | # latter to run on the former, but supporting this in 1.6 turns out to be super 110 | # painful. 111 | 112 | when (NimMajor, NimMinor) >= (1, 7): 113 | type 114 | LinearSequence*[T] {.pure, inheritable.} = object 115 | ## Represents the linear sequence `initial + stride * k` for a variable `k`. 116 | ## 117 | ## One could also think of it as an infinite loop with a linear index 118 | ## variable. 119 | initial*: T 120 | stride*: T 121 | 122 | when (NimMajor, NimMinor) < (1, 7): 123 | import std/macros except last 124 | # Ugly hack to get around the fact that we need to use a deprecated syntax 125 | # which doesn't parse at all in Nim 1.7... 126 | # 127 | # And this deprecated syntax is needed due to another bug in the Nim compiler: 128 | # https://github.com/nim-lang/Nim/issues/16653 129 | # 130 | macro declTypeHack(): untyped = 131 | nnkStmtList.newTree( 132 | nnkTypeSection.newTree( 133 | nnkTypeDef.newTree( 134 | nnkPragmaExpr.newTree( 135 | nnkPostfix.newTree(newIdentNode("*"), newIdentNode("LinearSequence")), 136 | nnkPragma.newTree(newIdentNode("pure")) 137 | ), 138 | nnkGenericParams.newTree( 139 | nnkIdentDefs.newTree(newIdentNode("T"), newEmptyNode(), newEmptyNode()) 140 | ), 141 | nnkObjectTy.newTree( 142 | nnkPragma.newTree(newIdentNode("inheritable")), 143 | newEmptyNode(), 144 | nnkRecList.newTree( 145 | nnkIdentDefs.newTree( 146 | nnkPostfix.newTree(newIdentNode("*"), newIdentNode("initial")), 147 | newIdentNode("T"), 148 | newEmptyNode() 149 | ), 150 | nnkIdentDefs.newTree( 151 | nnkPostfix.newTree(newIdentNode("*"), newIdentNode("stride")), 152 | newIdentNode("T"), 153 | newEmptyNode() 154 | ) 155 | ) 156 | ) 157 | ) 158 | ) 159 | ) 160 | 161 | declTypeHack() 162 | ## .. note:: If you're using Nim 1.6 or older the documentation for `LinearSequence` won't 163 | ## show up properly below. The reasons for this is a vortex of pain. To compile 164 | ## the type a certain deprecated syntax is neeeded (due to a compiler bug) which 165 | ## doesn't compile at all in Nim 1.7, so it's just generated by a macro instead, 166 | ## and thus docs are missing. See source. 167 | 168 | 169 | type 170 | LinearSegment*[T, I] {.pure.} = object of LinearSequence[T] 171 | ## A `LinearSequence` with a `count`: a finite linear sequence. 172 | ## 173 | ## This is meant to abstracts a general indexing loop, and iterating over it 174 | ## produces the following loop: 175 | ## 176 | ## .. code-block:: 177 | ## var index = 178 | ## let stop = + * 179 | ## while index != stop: 180 | ## yield index 181 | ## index.inc 182 | ## 183 | ## See `StridedSlice` for a different kind of representation. 184 | ## 185 | ## Note: `segment[i]` is not bound checked. Can be constrained with 186 | ## `segment[a .. b]`. 187 | ## 188 | count*: I 189 | 190 | static: 191 | assert sizeof(LinearSegment[int, int]) == sizeof(int) * 3 192 | 193 | func initLinearSequence*[T](initial, stride: T): LinearSequence[T] {.inline.} = 194 | result.initial = initial 195 | result.stride = stride 196 | 197 | func initLinearSequence*[T](stride: T): LinearSequence[T] {.inline.} = 198 | initLinearSequence(0, stride) 199 | 200 | func initLinearSegment*[T, I](initial, stride: T; count: I): LinearSegment[T, I] {.inline.} = 201 | result.initial = initial 202 | result.stride = stride 203 | result.count = count 204 | 205 | func `$`*(ls: LinearSequence): string = 206 | &"LinearSequence({ls.stride}*x + {ls.initial})" 207 | 208 | func `$`*(seg: LinearSegment): string = 209 | &"LinearSegment({seg.initial} + {seg.stride} * (0 ..< {seg.count}))" 210 | 211 | converter toTuple*[T](ls: LinearSequence[T]): (T, T) = 212 | (ls.initial, ls.stride) 213 | 214 | converter toTuple*[T, I](seg: LinearSegment[T, I]): (T, T, I) = 215 | (seg.initial, seg.stride, seg.count) 216 | 217 | iterator items*[T](ls: LinearSequence[T]): T {.inline.} = 218 | ## Infinite loop over the linear sequence `ls[0], ls[1], ls[2], ...`. 219 | var value = ls.initial 220 | while true: 221 | yield value 222 | value.inc ls.stride 223 | 224 | iterator items*(seg: LinearSegment): auto {.inline.} = 225 | ## Yields `seg[0], seg[1], ..., seg[seg.len - 1]`. 226 | let stop = seg[seg.count] 227 | var value = seg.initial 228 | while value != stop: 229 | yield value 230 | value.inc seg.stride 231 | 232 | iterator pairs*(seg: LinearSegment): auto {.inline.} = 233 | ## Yields the sequence `(k, seg[k])` for `k in 0 ..< seg.len`. 234 | var value = seg.initial 235 | for i in 0 ..< seg.len: 236 | yield (i, value) 237 | value.inc seg.stride 238 | 239 | 240 | func numStrides(stride, first, last, sadj: distinct SomeInteger): auto {.inline.} = 241 | case stride: 242 | of 0: raise newException(ValueError, "linear sequence is degenerate") 243 | of 1: last - first 244 | of -1: first - last 245 | else: (last - first + sadj).euclDiv(stride) 246 | 247 | 248 | 249 | 250 | 251 | type 252 | StridedSlice*[T, U] = object 253 | ## Strided slice. Acts like a `HSlice` but with a stride parameter (`s`). 254 | ## 255 | ## Inspiration is Python's slices and `range`, though the end point is still 256 | ## (unfortunately) counted as inclusive in order to remain compatible with 257 | ## `HSlice`. 258 | ## 259 | ## You would normally construct these with the `@:`_ operator and resolve them into 260 | ## concrete `LinearSegment`s with the `^!`_ operator. 261 | ## 262 | ## .. warning:: The stride should never be 0. 263 | ## 264 | a*: T 265 | b*: U 266 | s*: int 267 | 268 | StridedIndex* = distinct int ## Represents a `@:stride` index. 269 | 270 | func initStridedSlice*[T, U](a: T, b: U, s: int): StridedSlice[T, U] {.inline.} = 271 | StridedSlice[T, U](a: a, b: b, s: s) 272 | 273 | func `$`*(ss: StridedSlice): string = 274 | &"({$ss.a} .. {$ss.b} @: {$ss.s})" 275 | 276 | func `$`*(si: StridedIndex): string = 277 | &"@:{si.int}" 278 | 279 | converter toTuple*[T, I](ss: StridedSlice[T, I]): (T, T, I) = 280 | (ss.a, ss.b, ss.s) 281 | 282 | converter toStridedSlice*(seg: LinearSegment): auto = 283 | initStridedSlice(seg.initial, seg.last, seg.stride) 284 | 285 | func toLinearSegment*(ss: StridedSlice): auto {.inline.} = 286 | ## Converts a *resolved* `StridedSlice`_ to a `LinearSegment`_. 287 | when ss.a is BackwardsIndex or ss.b is BackwardsIndex: 288 | {.error "strided slice must be resolved to convert it to a linear segment".} 289 | let (a, b, s) = ss.toTuple() 290 | assert s != 0, "stride cannot be zero" 291 | let c = if s < 0: 292 | numStrides(s, a, b - 1, 0) 293 | else: 294 | numStrides(s, a, b + 1, s - 1) 295 | initLinearSegment(a, s, c.max(0)) 296 | 297 | 298 | 299 | iterator items*(ss: StridedSlice): auto {.inline.} = 300 | when ss.a isnot SomeInteger or ss.b isnot SomeInteger: 301 | {.error "strided slice must be resolved to iterate over it".} 302 | 303 | for i in ss.toLinearSegment(): 304 | yield i 305 | 306 | type 307 | AnyStrided* = ## Any type that can commonly be used to slice with a stride. 308 | StridedSlice or StridedIndex or LinearSegment 309 | AnyIndexing* = ## Any type that can commonly be used to index an array-like container. 310 | SomeInteger or BackwardsIndex or HSlice or AnyStrided 311 | 312 | func `^!`*(idx: AnyIndexing, length: SomeInteger): auto {.inline.} = 313 | ## Resolves indexing-like types. 314 | ## 315 | ## Given an integer or `BackwardsIndex` this acts like `^^`, but takes the 316 | ## length directly (as opposed to the container). 317 | ## 318 | ## Given a `HSlice` it resolves any contained `BackwardsIndex` to `int`. 319 | ## 320 | ## Given a `StridedSlice` or `StridedIndex` it converts it to a `LinearSegment`. 321 | ## 322 | ## Slices are *not* constrained to the given length. 323 | ## 324 | ## Normally the only use for this operator is in overloading `[]` to implement indexing. 325 | ## 326 | when idx is BackwardsIndex: 327 | length - idx.int 328 | elif idx is HSlice: 329 | idx.a ^! length .. idx.b ^! length 330 | elif idx is StridedIndex: 331 | let s = idx.int 332 | let (a, c) = if s < 0: 333 | (length - 1, numStrides(s, length, 0, 0)) 334 | else: 335 | (0, numStrides(s, 0, length, s - 1)) 336 | initLinearSegment(a, s, c.max(0)) 337 | elif idx is StridedSlice: 338 | (idx.a ^! length .. idx.b ^! length @: idx.s).toLinearSegment() 339 | else: # SomeInteger or LinearSegment 340 | idx 341 | 342 | func `^?`*(idx: AnyIndexing, length: SomeInteger): auto {.inline.} = 343 | ## Resolves indexing-like types, and constrains slicing types so they cannot 344 | ## go out-of-bounds. 345 | ## 346 | ## This functions much like `^!`_, but slices (`HSlice`, `StridedSlice`, 347 | ## `SliceIndex`, `LinearSegment`) will automatically be constrained so they 348 | ## cannot go out-of-bounds. 349 | ## 350 | ## Normally the only use for this operator is in overloading `[]` to implement indexing. 351 | ## 352 | ## .. note:: The `^?` and `^!` operators have higher precedence than `..`, so 353 | ## parenthesis is needed when using them with a literal slice. 354 | runnableExamples: 355 | assert (^1) ^? 50 == 49 356 | assert (0 .. 9) ^? 50 == 0 .. 9 357 | 358 | assert (20 .. 45) ^? 30 == 20 .. 29 359 | assert (0 .. 99 @: 20) ^? 50 == initLinearSegment(0, 20, 3) 360 | assert (@: -1) ^? 10 == initLinearSegment(9, -1, 10) 361 | 362 | let resolved = idx ^! length 363 | 364 | when resolved is HSlice: 365 | resolved.a.max(0) .. resolved.b.min(length - 1) 366 | elif resolved is LinearSegment: 367 | resolved[0 ..< length] 368 | else: 369 | resolved 370 | 371 | func `@:`*[T, U](slice: HSlice[T, U], step: int): StridedSlice[T, U] {.inline.} = 372 | ## Turns a regular slice into a strided slice. 373 | ## 374 | ## .. note:: For slices with negative stride (i.e. right-to-left slices) the 375 | ## largest number should be specified first: `100 .. 0 @: -1` represents the 376 | ## indices `100, 99, 98, ..., 0`, but `0 .. 100 @: -1` is empty. 377 | ## 378 | StridedSlice[T,U](a: slice.a, b: slice.b, s: step) 379 | 380 | func `@:`*[T: SomeInteger](e: T, step: int): StridedSlice[T, int] {.inline.} = 381 | ## Constructs a strided slice with a length and a step. 382 | ## 383 | ## The number is taken as a "total length" of a hypothetical `0 ..< L` slice. 384 | ## Thus it is interpreted as an *exclusive* end point when the stride is positive, and 385 | ## an exclusive *start point* when stride is negative. 386 | ## 387 | ## The number of elements in the strided slice is easy to calculate as 388 | ## simply `length div step`. 389 | ## 390 | runnableExamples: 391 | assert (12 @: 2) == (0 .. 11 @: 2) 392 | assert (12 @: -2) == (11 .. 0 @: -2) 393 | 394 | # Note that these differ in more than direction! One former touches only 395 | # even numbers, the latter only odd numbers. 396 | 397 | assert e >= 0, "invalid length" 398 | if step >= 0: 399 | StridedSlice[T, int](a: T(0), b: e.pred, s: step) 400 | else: 401 | StridedSlice[T, int](a: e.pred, b: 0, s: step) 402 | 403 | func `@:`*(step: int): auto {.inline.} = 404 | ## Prefix variant of the step operator. Equivalent to applying a stride to 405 | ## the entire valid length. Returns a `StridedIndex`. 406 | ## 407 | assert step != 0, "stride cannot be zero" 408 | StridedIndex(step) 409 | 410 | 411 | 412 | func maxLT*[T](ls: LinearSequence[T], bound: T): auto {.inline.} = 413 | ## Finds `k` such that `ls[k]` is the largest value in the sequence less than 414 | ## `bound`. 415 | numStrides(ls.stride, ls.initial, bound - 1, 0) 416 | 417 | func maxLTE*[T](ls: LinearSequence[T], bound: T): auto {.inline.} = 418 | ## Finds `k` such that `ls[k]` is the largest value in the sequence less than 419 | ## or equal to `bound`. 420 | numStrides(ls.stride, ls.initial, bound, 0) 421 | 422 | func minGT*[T](ls: LinearSequence[T], bound: T): auto {.inline.} = 423 | ## Finds `k` such that `ls[k]` is the least value in the sequence greater than 424 | ## `bound`. 425 | numStrides(ls.stride, ls.initial, bound + 1, ls.stride.abs - 1) 426 | 427 | func minGTE*[T](ls: LinearSequence[T], bound: T): auto {.inline.} = 428 | ## Finds `k` such that `ls[k]` is the least value in the sequence greater or 429 | ## equal to `bound`. 430 | numStrides(ls.stride, ls.initial, bound, ls.stride.abs - 1) 431 | 432 | 433 | 434 | func len*(seg: LinearSegment): auto {.inline.} = 435 | ## Number of values in this sequence. 436 | ## 437 | ## Synonym for `seg.count`. 438 | seg.count 439 | 440 | func last*(seg: LinearSegment): auto {.inline.} = 441 | ## The last value in this sequence. 442 | ## 443 | ## Synonym for `seg[seg.len - 1]`. 444 | ## 445 | ## .. warning:: Invalid if `seg.len == 0`. 446 | seg.initial + seg.stride * (seg.count - 1) 447 | 448 | 449 | 450 | 451 | func `[]`*[T](ls: LinearSequence[T], k: T): auto {.inline.} = 452 | ## Gives the `k`th value in this sequence. 453 | ## 454 | ## Synonym for `ls.initial + ls.stride * k`. 455 | ## 456 | ## .. note:: 0-indexed, so the first value is `ls[0]`. 457 | ## 458 | ls.initial + ls.stride * k 459 | 460 | func `[]`*[T](ls: LinearSequence[T], slice: HSlice): LinearSegment[T, T] {.inline.} = 461 | let 462 | ak = ls.minGTE(slice.a) 463 | bk = ls.maxLT(slice.b + 1) 464 | 465 | assert ls[ak] >= slice.a 466 | assert ls[bk] < slice.b + 1 467 | 468 | result.initial = ls[min(ak, bk)] 469 | result.stride = ls.stride 470 | if result.initial in slice: 471 | result.count = (ak - bk).abs + 1 472 | else: 473 | result.count = 0 474 | 475 | func `-`(seg: LinearSegment): auto = 476 | result = seg 477 | result.initial = -result.initial 478 | result.stride = -result.stride 479 | 480 | func `[]`*(seg: LinearSegment, slice: HSlice): auto {.inline.} = 481 | ## Constrains a `LinearSegment` to its overlap with the given slice. 482 | runnableExamples: 483 | let seg = initLinearSegment(1, 3, 6) # [1,4,7,10,13,16] 484 | 485 | assert seg[5 .. 20].toStridedSlice == (7 .. 16 @: 3) 486 | assert seg[-10 .. 0].toStridedSlice == (1 .. -2 @: 3) 487 | assert seg[10 .. 12].toStridedSlice == (10 .. 10 @: 3) 488 | 489 | if seg.count == 0: 490 | return seg 491 | 492 | if seg.stride < 0: 493 | return -(-seg)[ -slice.b .. -slice.a ] 494 | 495 | var seg = seg 496 | if seg.initial < slice.a: 497 | let k = seg.minGTE(slice.a) 498 | seg.initial = seg[k] 499 | seg.count -= k 500 | 501 | if seg.last > slice.b: 502 | seg.count = seg.minGT(slice.b) 503 | 504 | if seg.count < 0: 505 | seg.count = 0 506 | seg 507 | 508 | func `[]`*[T](arr: openArray[T], seg: AnyStrided): seq[T] = 509 | ## Overload so that `StridedSlice` and `StridedIndex` can be used with arrays 510 | ## and the `seq` data type. 511 | runnableExamples: 512 | assert [1,2,3,4][ @:2 ] == @[1, 3] 513 | 514 | when seg isnot LinearSegment: 515 | let seg = seg ^? arr.len 516 | result = newSeqOfCap[T](seg.count) 517 | for i in seg: 518 | result.add(arr[i]) 519 | 520 | func `[]`*(s: string, seg: AnyStrided): string = 521 | ## Overload so that `StridedSlice` and `StridedIndex` can be used with 522 | ## strings. 523 | runnableExamples: 524 | assert "nefarious"[ 2 .. 0 @: -1 ] == "fen" 525 | 526 | when seg isnot LinearSegment: 527 | let seg = seg ^? s.len 528 | result = newStringOfCap(seg.count) 529 | for i in seg: 530 | result.add(s[i]) 531 | 532 | template assImpl(dest, seg, src: untyped): untyped = 533 | when seg isnot LinearSegment: 534 | let seg = seg ^? s.len 535 | 536 | if seg.stride == -1: 537 | var input = input[0 .. ^1] 538 | input.reverse() 539 | dest[seg.initial - seg.count + 1 .. seg.initial] = input 540 | elif seg.stride == 1: 541 | dest[seg.initial .. seg.initial + seg.count - 1] = input 542 | else: 543 | if input.len != seg.count: 544 | raise newException(ValueError, "source doesn't match the length of the slice") 545 | for (c, i) in pairs(seg): 546 | dest[i] = input[c] 547 | 548 | func `[]=`*(s: var string, seg: AnyStrided, input: string) = 549 | ## Overload so that `StridedSlice` and `StridedIndex` can be used with 550 | ## strings. 551 | runnableExamples: 552 | var s = "nefarious" 553 | 554 | s[ 2 .. 0 @: -1 ] = "gerg" 555 | assert s == "gregarious" # note the reversed order of 'gerg' 556 | 557 | s[@:3] = "xxxx" 558 | assert s == "xrexarxoux" 559 | 560 | assImpl(s, seg, input) 561 | 562 | func `[]=`*[T](s: var seq[T], seg: AnyStrided, input: openArray[T]) = 563 | ## Overload so that `StridedSlice` and `StridedIndex` can be used with 564 | ## strings. 565 | 566 | assImpl(s, seg, input) 567 | -------------------------------------------------------------------------------- /strides.nimble: -------------------------------------------------------------------------------- 1 | # Package 2 | 3 | version = "0.1.1" 4 | author = "Frank S. Hestvik" 5 | description = "Indexing and slicing with stride" 6 | license = "MIT" 7 | srcDir = "src" 8 | 9 | 10 | # Dependencies 11 | 12 | # note inheritable pragma with generics is bugged in stable 1.6. 13 | requires "nim >= 1.6" 14 | -------------------------------------------------------------------------------- /tests/config.nims: -------------------------------------------------------------------------------- 1 | switch("path", "$projectDir/../src") -------------------------------------------------------------------------------- /tests/t_strides.nim: -------------------------------------------------------------------------------- 1 | import std/strformat 2 | import std/unittest 3 | import std/random 4 | import std/sequtils 5 | import std/sugar 6 | 7 | 8 | import strides 9 | 10 | suite "strides slices": 11 | 12 | test "string": 13 | 14 | let greet = "hello world" 15 | 16 | # As a prefix: assumes the whole length. 17 | check greet[ @: 2 ] == "hlowrd" 18 | check greet[ @: -1 ] == "dlrow olleh" 19 | 20 | # As a strepped slice: 21 | check greet[ 2 .. 8 @: 3 ] == "l r" 22 | 23 | # As a length + step. 24 | check greet[ 5 @: 1 ] == "hello" 25 | check greet[ 5 @: 2 ] == "hlo" 26 | check greet[ 5 @: -1 ] == "olleh" 27 | 28 | test "lenient": 29 | let greet = "hello world" 30 | 31 | check greet[ ^2 .. 100 @: 1 ] == "ld" 32 | 33 | test "openarray": 34 | let nums = (0 .. 20).toSeq() 35 | check nums[1 .. 20 @: 7] == @[ 1, 8, 15 ] 36 | 37 | test "slice <-> segment": 38 | let seg = initLinearSegment(1, 3, 6) # [1,4,7,10,13,16] 39 | 40 | check seg.toStridedSlice == (1 .. 16 @: 3) 41 | 42 | check seg[5 .. 20].toStridedSlice == (7 .. 16 @: 3) 43 | check seg[-10 .. 0].toStridedSlice == (1 .. -2 @: 3) 44 | check seg[10 .. 12].toStridedSlice == (10 .. 10 @: 3) 45 | 46 | check ((1 .. 20 @: 7) ^! 0).toSeq() == @[ 1, 8, 15 ] 47 | check ((1 .. 20 @: 7) ^? 0).toSeq().len == 0 48 | 49 | test "intro": 50 | 51 | let text = "hello world" 52 | 53 | # Regular slice 54 | check text[ 2 .. ^3 ] == "llo wor" 55 | 56 | # Strided slice with a step of 2. 57 | check text[ 2 .. ^3 @: 2 ] == "lowr" 58 | 59 | # Strided slice with reversed direction. 60 | check text[ ^3 .. 2 @: -1 ] == "row oll" 61 | 62 | # Just the stride, like `xs[::s]` in Python: 63 | check text[ @: -1 ] == "dlrow olleh" 64 | check text[ @: 2 ] == "hlowrd" 65 | 66 | # Additionally, a third form of length+stride is supported: 67 | 68 | # Positive stride works like `xs[:a:s]` in Python: 69 | check text[ 10 @: 1 ] == "hello worl" 70 | check text[ 10 @: 2 ] == "hlowr" 71 | check text[ 10 @: 3 ] == "hlwl" 72 | 73 | # Negative stride works like `xs[a::s]` in Python: 74 | check text[ 10 @: -1 ] == "lrow olleh" 75 | check text[ 10 @: -2 ] == "lo le" 76 | check text[ 10 @: -3 ] == "lwlh" 77 | 78 | test "iterators": 79 | 80 | let k1 = collect: 81 | for i in 0 ..< 10 @: 2: 82 | i 83 | 84 | check k1 == @[0, 2, 4, 6, 8] 85 | 86 | let k2 = collect: 87 | for i in 20 .. -1 @: -7: 88 | i 89 | 90 | check k2 == @[20, 13, 6, -1] 91 | 92 | suite "linear sequence": 93 | setup: 94 | randomize( 0xdead_decade_dead ) 95 | var lss = @[ initLinearSequence(0, 1), 96 | initLinearSequence(-2, -2), 97 | initLinearSequence(0, 3), 98 | initLinearSequence(0, -3), 99 | initLinearSequence(-1, 17), 100 | initLinearSequence(77, -7), 101 | initLinearSequence(-2, 2), 102 | initLinearSequence(10, -5), ] 103 | 104 | for _ in 1 .. 10: 105 | lss.add(initLinearSequence( rand(-10 .. 10), rand(1 .. 20) )) 106 | lss.add(initLinearSequence( rand(-10 .. 10), rand(-20 .. -1) )) 107 | 108 | test "index": 109 | for ls in lss: 110 | for i in -5 .. 5: 111 | check ls[i] == ls.stride * i + ls.initial 112 | 113 | test "bounds": 114 | for ls in lss: 115 | for i in -10 .. 10: 116 | let k = ls.maxLT(i) 117 | check ls[k] < i 118 | check ls[k+1] >= i or ls[k+1] < ls[k] 119 | check ls[k-1] >= i or ls[k-1] < ls[k] 120 | 121 | for i in -10 .. 10: 122 | let k = ls.minGTE(i) 123 | check ls[k] >= i 124 | check ls[k+1] < i or ls[k+1] > ls[k] 125 | check ls[k-1] < i or ls[k-1] > ls[k] 126 | 127 | proc randslice(reach: int): auto = 128 | let l = rand(-reach .. reach) 129 | let u = rand(-reach .. reach) 130 | min(l, u) .. max(l, u) 131 | 132 | test "slicing": 133 | for ls in lss: 134 | for _ in 1 .. 10_000: 135 | let r1 = randslice(200) 136 | 137 | let seg = ls[r1] 138 | check seg is LinearSegment 139 | let (start, stride, count) = seg.toTuple() 140 | 141 | check count >= 0 142 | if count > 0: 143 | check start in r1 144 | check start + (count - 1) * stride in r1 145 | check start + count * stride notin r1 146 | check start - stride notin r1 147 | else: 148 | check r1.len < ls.stride.abs 149 | 150 | let r2 = randslice(150) 151 | let lim = seg[r2] 152 | 153 | check lim.len <= seg.len 154 | if lim.len > 0: 155 | if seg.initial in r2: 156 | check lim.initial == seg.initial 157 | if seg.last in r2: 158 | check lim.initial == seg.initial 159 | check lim.count == seg.count 160 | 161 | check lim.initial in r2 162 | check lim.last in r2 163 | else: 164 | check (r1 & r2).len < ls.stride.abs 165 | 166 | suite "other": 167 | test "$": 168 | check $(@:2) == "@:2" 169 | check $(^2) == "^2" 170 | check $(^1 .. 0 @: -5) == "(^1 .. 0 @: -5)" 171 | --------------------------------------------------------------------------------