{"id":17112998,"url":"https://github.com/emmt/easyranges.jl","last_synced_at":"2025-04-09T20:33:44.530Z","repository":{"id":61800244,"uuid":"506808539","full_name":"emmt/EasyRanges.jl","owner":"emmt","description":"Readable and efficient range expressions for loops in Julia","archived":false,"fork":false,"pushed_at":"2025-02-14T17:31:02.000Z","size":86,"stargazers_count":6,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-03-23T22:34:37.697Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Julia","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/emmt.png","metadata":{"files":{"readme":"README.md","changelog":"NEWS.md","contributing":null,"funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2022-06-23T22:26:23.000Z","updated_at":"2025-02-14T17:09:46.000Z","dependencies_parsed_at":"2024-11-10T15:26:29.683Z","dependency_job_id":"c5ace941-d9fd-4b52-9b09-71e7a635cf83","html_url":"https://github.com/emmt/EasyRanges.jl","commit_stats":{"total_commits":27,"total_committers":1,"mean_commits":27.0,"dds":0.0,"last_synced_commit":"f17ca035b7d4150bb2d3c2d30a6ea489c9419a51"},"previous_names":[],"tags_count":5,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/emmt%2FEasyRanges.jl","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/emmt%2FEasyRanges.jl/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/emmt%2FEasyRanges.jl/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/emmt%2FEasyRanges.jl/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/emmt","download_url":"https://codeload.github.com/emmt/EasyRanges.jl/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248107861,"owners_count":21049022,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-10-14T17:02:12.332Z","updated_at":"2025-04-09T20:33:44.501Z","avatar_url":"https://github.com/emmt.png","language":"Julia","funding_links":[],"categories":[],"sub_categories":[],"readme":"# EasyRanges: range expressions made easier for Julia\n\n[![Build Status](https://github.com/emmt/EasyRanges.jl/actions/workflows/CI.yml/badge.svg?branch=main)](https://github.com/emmt/EasyRanges.jl/actions/workflows/CI.yml?query=branch%3Amain)\n[![Build Status](https://ci.appveyor.com/api/projects/status/github/emmt/EasyRanges.jl?svg=true)](https://ci.appveyor.com/project/emmt/EasyRanges-jl)\n[![Coverage](https://codecov.io/gh/emmt/EasyRanges.jl/branch/main/graph/badge.svg)](https://codecov.io/gh/emmt/EasyRanges.jl)\n\n`EasyRanges` is a small Julia package dedicated at making life easier with integer or\nCartesian indices and ranges. This package exports macros `@range` and `@reverse_range`\nwhich take an expression with extended syntax rules (see below) and rewrite it to produce\nan `Int`-valued *index range* which may be a step range or an instance of\n`CartesianIndices`. These two macros differ in the step sign of the result: `@range`\nalways yield ranges with non-decreasing indices, while `@reverse_range` always yield\nranges with non-increasing indices.\n\nCompared to range expressions with broadcast operators (`.+`, `.-`, etc.) that are\nimplemented by Julia, the `EasyRanges` package offers a number of advantages:\n\n- The code is more expressive and an extended syntax is supported.\n\n- Computing the resulting range can be much faster and involves at most `O(d)` storage\n  with `d` the number of array dimensions. Note: Julia ≥ 1.9 improves on this by being\n  able to return an iterator, yet expressions such as `A ∩ (I .- B)`, with `A` and `B`\n  Cartesian ranges and `I` a Cartesian index, yield an array of Cartesian indices.\n\n- The `@range` macro always yields non-decreasing indices which is most favorable for the\n  efficiency of **loop vectorization**, for example with the `@simd` macro of Julia or\n  with with the `@turbo` (formerly `@avx`) macro provided by\n  [`LoopVectorization`](https://github.com/JuliaSIMD/LoopVectorization.jl.git).\n\nTable of contents:\n\n- [Usage](#usage)\n  - [Definitions](#definitions)\n  - [Shift operations](#shift-operations)\n  - [Intersecting](#intersecting)\n  - [Stretching](#stretching)\n  - [Shrinking](#shrinking)\n- [Extension to other types](#extension-to-other-types)\n- [A working example](#a-working-example)\n- [Installation](#installation)\n\n## Usage\n\n```julia\nusing EasyRanges\n```\n\nbrings two macros, `@range` and `@reverse_range`, into scope. These macros can be used as:\n\n```julia\n@range expr\n@reverse_range expr\n```\n\nto evaluate expression `expr` with special rules (see below) where integers, Cartesian\nindices, and ranges of integers or of Cartesian indices are treated specifically:\n\n- integers are converted to `Int`, ranges to `Int`-valued ranges, and tuples of integers\n  to tuples of `Int`;\n\n- arithmetic expressions only involving indices and ranges yield lightweight and efficient\n  ranges (of integers or of Cartesian indices);\n\n- ranges produced by `@range` (resp. `@reverse_range`) always have positive (resp.\n  negative) steps;\n\n- operators `+` and `-` can be used to [*shift*](#shift-operations) index ranges;\n\n- operator `∩` and method `intersect` yield the [intersection](#intersecting) of ranges\n  with ranges, of ranges with indices, or of indices with indices;\n\n- operator `±` can be used to [*stretch*](#stretching) ranges or to produce centered\n  ranges;\n\n- operator `∓` can be used to [*shrink*](#shrinking) ranges;\n\n- the syntax `$(subexpr)` may be used to prevent any sub-expression `subexpr` of `expr`\n  from being interpreted as a range expression, i.e. with the above rules.\n\nAs shown in [*A working example*](#a-working-example) below, these rules are useful for\nwriting readable ranges in `for` loops without sacrificing efficiency.\n\n\n### Definitions\n\nIn `EasyRanges`, if *indices* are integers, *ranges* means ranges of integers (of\nsuper-type `OrdinalRange{Int}`); if *indices* are Cartesian indices, *ranges* means ranges\nof Cartesian indices (of super-type `CartesianIndices`).\n\n\n### Shift operations\n\nIn `@range` and `@reverse_range` expressions, an index range `R` can be shifted with the\noperators `+` and `-` by an amount specified by an index `I`:\n\n```julia\n@range R + I -\u003e S # J ∈ S is equivalent to J - I ∈ R\n@range R - I -\u003e S # J ∈ S is equivalent to J + I ∈ R\n\n@range I + R -\u003e S # J ∈ S is equivalent to J - I ∈ R\n@range I - R -\u003e S # J ∈ S is equivalent to I - J ∈ R\n```\n\nInteger-valued ranges can be shifted by an integer offset:\n\n```julia\n@range (3:6) + 1    -\u003e  4:7    # (2:6) .+ 1    -\u003e  4:7\n@range 1 + (3:6)    -\u003e  4:7    # (2:6) .+ 1    -\u003e  4:7\n@range (2:4:10) + 1 -\u003e  3:4:11 # (2:4:10) .+ 1 -\u003e  3:4:11\n@range (3:6) - 1    -\u003e  2:5    # (3:6) .- 1    -\u003e  2:5\n@range 1 - (3:6)    -\u003e -5:-2   # 1 .- (3:6)    -\u003e -2:-1:-5\n```\n\nThis is like using the broadcasting operators `.+` and `.-` except that the result is an\n`Int`-valued range and that the step sign is kept positive (as in the last above example).\n\nThe `@reverse_macro` yields ranges with negative steps:\n\n```julia\n@reverse_range (3:6) + 1 -\u003e  7:-1:4\n@reverse_range 1 + (3:6) -\u003e  7:-1:4\n@reverse_range (3:6) - 1 -\u003e  5:-1:2\n@reverse_range 1 - (3:6) -\u003e -2:-1:-5\n```\n\nCartesian ranges can be shifted by a Cartesian index (without penalties on the execution\ntime and, usually, no extra allocations):\n\n```julia\n@range CartesianIndices((2:6, -1:2)) + CartesianIndex(1,3)\n# -\u003e CartesianIndices((3:7, 2:5))\n@range CartesianIndex(1,3) + CartesianIndices((2:6, -1:2))\n# -\u003e CartesianIndices((3:7, 2:5))\n@range CartesianIndices((2:6, -1:2)) - CartesianIndex(1,3)\n# -\u003e CartesianIndices((1:5, -4:-1))\n@range CartesianIndex(1,3) - CartesianIndices((2:6, -1:2))\n# -\u003e CartesianIndices((-5:-1, 1:4))\n```\n\nThis is similar to the broadcasting operators `.+` and `.-` except that a lightweight\ninstance of `CartesianIndices` with positive increment is always produced.\n\n\n### Intersecting\n\nIn `@range` and `@reverse_range` expressions, the operator `∩` (obtained by typing `\\cap`\nand pressing the `[tab]` key at the REPL) and the method `intersect` yield the\nintersection of ranges with ranges, of ranges with indices, or of indices with indices.\n\nThe intersection of indices, say `I` and `J`, yield a range `R` (empty if the integers are\ndifferent):\n\n```julia\n@range I ∩ J -\u003e R   # R = {I} if I == J, R = {} else\n```\n\nExamples:\n\n```julia\n@range 3 ∩ 3 -\u003e 3:3\n@range 3 ∩ 2 -\u003e 1:0  # empty range\n@range CartesianIndex(3,4) ∩ CartesianIndex(3,4) -\u003e CartesianIndices((3:3,4:4))\n```\n\nThe intersection of an index range `R` and an index `I` yields an index range `S` that is\neither the singleton `{I}` (if `I` belongs to `R`) or empty (if `I` does not belong to\n`R`):\n\n```julia\n@range R ∩ I -\u003e S   # S = {I} if I ∈ R, S = {} else\n@range I ∩ R -\u003e S   # idem\n```\n\nExamples:\n\n```julia\n@range (2:6) ∩ 3     -\u003e 3:3 # a singleton range\n@range 1 ∩ (2:6)     -\u003e 1:0 # an empty range\n@range (2:6) ∩ (3:7) -\u003e 3:6 # intersection of ranges\n@range CartesianIndices((2:4, 5:9)) ∩ CartesianIndex(3,7)\n    -\u003e CartesianIndices((3:3, 7:7))\n```\n\nThese syntaxes are already supported by Julia, but the `@range` macro guarantees to return\nan `Int`-valued range with a forward (positive) step.\n\n\n### Stretching\n\nIn `@range` and `@reverse_range` expressions, the operator `±` (obtained by typing `\\pm`\nand pressing the `[tab]` key at the REPL) can be used to **stretch** ranges or to produce\n**centered ranges**.\n\nThe expression `R ± I` yields the index range `R` stretched by an amount specified by\nindex `I`. Assuming `R` is unit range:\n\n```julia\n@range R ± I -\u003e (first(R) - I):(last(R) + I)\n```\n\nwhere, if `R` is a range of integers, `I` is an integer, and if `R` is a `N`-dimensional\nCartesian, `I` is a `N`-dimensional Cartesian index range. Not shown in the above\nexpression, the range step is preserved by the operation (except that the result has a\npositive step).\n\nThe expression `I ± ΔI` with `I` an index and `ΔI` an index offset yields an index range\ncentered at `I`. Assuming `R` is unit range:\n\n```julia\n@range I ± ΔI -\u003e (I - ΔI):(I + ΔI)\n```\n\nThere is no sign correction and the range may be empty. If `I` and `ΔI` are two integers,\n`I ± ΔI` is a range of integers. If `I` is a `N`-dimensional Cartesian index, then `I ±\nΔI` is a range of Cartesian indices and `ΔI` can be an integer, a `N`-tuple of integers,\nor a `N`-dimensional Cartesian index. Specifying `ΔI` as a single integer for a\n`N`-dimensional Cartesian index `I` is identical to specifying the same amount of\nstretching for each dimension.\n\n\n### Shrinking\n\nIn `@range` and `@reverse_range` expressions, the operator `∓` (obtained by typing `\\mp`\nand pressing the `[tab]` key at the REPL) can be used to **shrink** ranges.\n\nThe expression `R ∓ I` yields the same result as `@range R ± (-I)`, that is the index\nrange `R` shrunk by an amount specified by index `I`:\n\n```julia\n@range R ∓ I -\u003e (first(R) + I):(last(R) - I)\n```\n\n\n## Extension to other types\n\nTo extend the `@range` and `@reverse_range` macros to foreign types that may represent\nindices or ranges of indices, it may be sufficient to specialize the\n`EasyRanges.normalize(x::T)` method for the type `T` so that it returns an object which\nrepresents the same (Cartesian) index or set of indices as `x` but in one of the following\ncanonical forms:\n\n- an integer `i::Int` if `x` is equivalent to a single linear index;\n\n- a range of integers `r::AbstractRange{Int}` if `x` is equivalent to a range of linear\n  indices;\n\n- a multi-dimensional Cartesian index `I::CartesianIndex{N}` if `x` is equivalent to a\n  single `N`-dimensional Cartesian index;\n\n- a multi-dimensional Cartesian range `R::CartesianIndices{N}` if `x` is equivalent to a\n  rectangular region of `N`-dimensional Cartesian indices.\n\nThis is all what is needed to have the `@range` and `@reverse_range` macros handle indices\nor index ranges of type `T`.\n\nThe following methods can also be specialized in the types of their arguments to make\n`EasyRanges` aware of types provided by foreign packages:\n\n- `EasyRanges.plus(x)` and `EasyRanges.plus(x, y)` implement `+x` and `x + y` in\n  `EasyRanges` expressions;\n\n- `EasyRanges.minus(x)` and `EasyRanges.minus(x, y)` implement `-x` and `x - y` in\n  `EasyRanges` expressions;\n\n- `EasyRanges.cap(a, b)` implements `a ∩ b` in `EasyRanges` expressions;\n\n- `EasyRanges.stretch(a, b)` implements `a ± b` in `EasyRanges` expressions;\n\n- `EasyRanges.shrink(a, b)` implements `a ∓ b` in `EasyRanges` expressions;\n\n\n## A working example\n\n`EasyRanges` may be very useful to write readable expressions in ranges used by `for`\nloops. For instance, suppose that you want to compute a **discrete correlation** of `A` by\n`B` as follows:\n\n$$\nC[i] = \\sum_{j} A[j] B[j-i]\n$$\n\nand for all valid indices `i` and `j`. Assuming `A`, `B` and `C` are abstract vectors, the\nJulia equivalent code is:\n\n```julia\nfor i ∈ eachindex(C)\n    s = zero(T)\n    j_first = max(firstindex(A), firstindex(B) + i)\n    j_last = min(lastindex(A), lastindex(B) + i)\n    for j ∈ j_first:j_last\n        s += A[j]*B[j-i]\n    end\n    C[i] = s\nend\n```\n\nwhere `T` is a suitable type, say `T = promote_type(eltype(A), eltype(B))`. The above\nexpressions of `j_first` and `j_last` are to ensure that `A[j]` and `B[j-i]` are in\nbounds. The same code for multidimensional arrays writes:\n\n```julia\nfor i ∈ CartesianIndices(C)\n    s = zero(T)\n    j_first = max(first(CartesianIndices(A)),\n                  first(CartesianIndices(B)) + i)\n    j_last = min(last(CartesianIndices(A)),\n                 last(CartesianIndices(B)) + i)\n    for j ∈ j_first:j_last\n        s += A[j]*B[j-i]\n    end\n    C[i] = s\nend\n```\n\nnow `i` and `j` are multidimensional Cartesian indices and Julia already helps a lot by\nmaking such a code applicable whatever the number of dimensions. Note that the syntax\n`j_first:j_last` is supported for Cartesian indices since Julia 1.1. There is more such\nsyntactic sugar and using the broadcasting operator `.+` and the operator `∩` (a shortcut\nfor the function `intersect`), the code can be rewritten as:\n\n```julia\nfor i ∈ CartesianIndices(C)\n    s = zero(T)\n    for j ∈ CartesianIndices(A) ∩ (CartesianIndices(B) .+ i)\n        s += A[j]*B[j-i]\n    end\n    C[i] = s\nend\n```\n\nwhich is not less efficient and yet much more readable. Indeed, the statement\n\n```julia\nfor j ∈ CartesianIndices(A) ∩ (CartesianIndices(B) .+ i)\n```\n\nmakes it clear that the loop is for all indices `j` such that `j ∈ CartesianIndices(A)`\nand `j - i ∈ CartesianIndices(B)` which is required to have `A[j]` and `B[j-i]` in bounds.\nThe same principles can be applied to the uni-dimensional code:\n\n```julia\nfor i ∈ eachindex(C)\n    s = zero(T)\n    for j ∈ eachindex(A) ∩ (eachindex(B) .+ i)\n        s += A[j]*B[j-i]\n    end\n    C[i] = s\nend\n```\n\nNow suppose that you want to compute the **discrete convolution** instead:\n\n$$\nC[i] = \\sum_{j} A[j] B[i-j]\n$$\n\nThen, the code for multi-dimensional arrays writes:\n\n```julia\nfor i ∈ CartesianIndices(C)\n    s = zero(T)\n    for j ∈ CartesianIndices(A) ∩ (i .- CartesianIndices(B))\n        s += A[j]*B[i-j]\n    end\n    C[i] = s\nend\n```\n\nbecause you want to have `j ∈ CartesianIndices(A)` and `i - j ∈ CartesianIndices(B)`, the\nlatter being equivalent to `j ∈ i - CartesianIndices(B)`.\n\nThis simple change however results in **a dramatic slowdown** because the expression `i .-\nCartesianIndices(B)` yields an array of Cartesian indices while the expression\n`CartesianIndices(B) .- i` yields an instance of `CartesianIndices`. As an example, the\ndiscrete convolution of a 32×32 array by a 8×8 array in single precision floating-point\ntakes 30.3 ms or 88.5 ms on my laptop (Intel Core i7-5500U CPU at 2.40GHz) depending on\nthe order of the operands and 40Mb of memory compared to 5.6 μs or 35.8 µs and no\nadditional memory for a discrete correlation (all with `@inbounds` and `@simd` of course).\nHence a slowdown by a factor of 5410 or 2570 for the same number of floating-point\noperations.\n\nUsing the `@range` macro of `EasyRanges`, the discrete correlation and discrete\nconvolution write:\n\n```julia\n# Discrete correlation.\nfor i ∈ CartesianIndices(C)\n    s = zero(T)\n    for j ∈ @range CartesianIndices(A) ∩ (i + CartesianIndices(B))\n        s += A[j]*B[j-i]\n    end\n    C[i] = s\nend\n\n# Discrete convolution.\nfor i ∈ CartesianIndices(C)\n    s = zero(T)\n    for j ∈ @range CartesianIndices(A) ∩ (i - CartesianIndices(B))\n        s += A[j]*B[i-j]\n    end\n    C[i] = s\nend\n```\n\nwhich do not require the broadcasting operators `.+` and `.-` and which do not have the\naforementioned issue. Using the macros `@range` and `@reverse_range` have other\nadvantages:\n\n- The result is guaranteed to be `Int`-valued (needed for efficient indexing).\n\n- The *step*, that is the increment between consecutive indices, in the result has a given\n  direction: `@range` always yields a non-negative step (which is favorable for loop\n  vectorization), while `@reverse_range` always yields a non-positive step.\n\n- The syntax of range expressions is simplified and extended for other operators (like `±`\n  for stretching or `∓` for shrinking) that are not available in the base Julia. This\n  syntax can be extended as the package is developed without disturbing other packages\n  (i.e., no type-piracy).\n\n\n## Installation\n\nThe `EasyRanges` package is an official Julia package and can be installed as follows:\n\n```julia\nusing Pkg\npkg\"add EasyRanges\"\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Femmt%2Feasyranges.jl","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Femmt%2Feasyranges.jl","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Femmt%2Feasyranges.jl/lists"}