Ranges

We invented the term, but it's just a fancy way to describe arrays of "from" — "to" string index ranges:

[
[3, 7],
[10, 15, "replacement"],
[4, 7]
]

[3, 7] means delete characters from index 3 to 7.

[10, 15, "replacement"] means replace characters from index 10 to 15 with replacement.

A case of no ranges is marked by null (but if you pass an empty array, it will work too).




That's all there is.



Same indexes as in String.prototype.slice().

Ranges gives us the flexibility to process strings. For example, detergent taps different packages, one to strip HTML, another to remove widow words and so on — they all report ranges. They all operate on the same source string, not on the source, mutated by the previous operation.

In the end, detergent merges those ranges and processes the string, rendering the result.

That's a different approach from the old way, mutating string over and over using regexes.

§ ranges-apply

It takes the source string and the amendments described by Ranges and produces a new string.

import { strict as assert } from "assert";
import applyR from "ranges-apply";

const oldString = `The quick brown fox jumps over the lazy dog.`;
const ranges = [
  [4, 19, "bad grey wolf"],
  [35, 43, "little Red Riding Hood"],
];
assert.equal(
  applyR(oldString, ranges),
  "The bad grey wolf jumps over the little Red Riding Hood."
);

See the package

§ ranges-push

When we want to gather ranges, instead of pushing them into an array, we can push them into this helper Class (for example, gatheredRanges below). That gives us automatic merging and sorting.

ranges-push also checks the types so it acts like a safeguard.

import { strict as assert } from "assert";
import Ranges from "ranges-push";
import applyR from "ranges-apply";

const gatheredRanges = new Ranges();

const oldString = `The quick brown fox jumps over the lazy dog.`;

// push the ranges
gatheredRanges.push(35, 43, "little Red Riding Hood");
gatheredRanges.push(4, 19, "bad grey wolf");

// retrieve the merged and sorted ranges by calling .current()
assert.deepEqual(gatheredRanges.current(), [
  [4, 19, "bad grey wolf"],
  [35, 43, "little Red Riding Hood"],
]);

assert.equal(
  applyR(oldString, gatheredRanges.current()),
  "The bad grey wolf jumps over the little Red Riding Hood."
);

// wipe all gathered ranges
gatheredRanges.wipe();
assert.equal(gatheredRanges.current(), null);

See the package

§ ranges-merge

If, after sorting, any two ranges in the vicinity have the same edge value (like 2 below), merge them.

import { strict as assert } from "assert";
import mergeR from "ranges-merge";

// joining edges:
assert.deepEqual(
  mergeR([
    [1, 2],
    [2, 3],
    [9, 10],
  ]),
  [
    [1, 3],
    [9, 10],
  ]
);

// an overlap:
assert.deepEqual(
  mergeR([
    [1, 5],
    [2, 10],
  ]),
  [[1, 10]]
);

See the package

§ ranges-sort

It sorts the ranges.

import { strict as assert } from "assert";
import rsort from "ranges-sort";

// Ranges (see codsen.com/ranges/) are sorted:
assert.deepEqual(
  rsort([
    [2, 3],
    [9, 10, "bad grey wolf"],
    [1, 2],
  ]),
  [
    [1, 2],
    [2, 3],
    [9, 10, "bad grey wolf"],
  ]
);

See the package

§ ranges-crop

It crops the ranges, ensuring no range from an array goes beyond a given index.

Along the way, it will also merge and sort ranges.

import { strict as assert } from "assert";
import crop from "ranges-crop";

assert.deepEqual(
  crop(
    [
      [2, 3],
      [9, 10, "bad grey wolf"],
      [1, 2],
    ],
    7
  ),
  [[1, 3]] // sorted, merged and cropped
);

See the package

§ ranges-regex

Takes a string, matches the given regex on it and returns ranges which would do the same thing.

Similarly to String.prototype.match(), a no-results case will yield null (which ranges-merge and others would gladly accept).

import { strict as assert } from "assert";
import raReg from "ranges-regex";

const oldString = `The quick brown fox jumps over the lazy dog.`;
const result = raReg(/the/gi, oldString);

// all regex matches, but in Ranges notation (see codsen.com/ranges/):
assert.deepEqual(result, [
  [0, 3],
  [31, 34],
]);

// if you slice the ranges, you'll get original regex caught values:
assert.deepEqual(
  result.map(([from, to]) =>
    oldString.slice(from, to)
  ),
  ["The", "the"]
);

See the package

§ ranges-ent-decode

This is a wrapper on top of market-leading HTML entity decoder he.js opens in a new tab decode() which returns ranges instead of string.

We tested the hell out of the code, directly and up-the-dependency-stream but as a cherry on top, all he.js opens in a new tab unit tests were ported to node-tap and do pass.

import { strict as assert } from "assert";
import decode from "ranges-ent-decode";

// see codsen.com/ranges/
assert.deepEqual(decode("a & b & c"), [
  [2, 8, "&"], // <--- that's Ranges notation, instructing to replace
  [11, 16, "&"],
]);

See the package

§ ranges-invert

Inverts ranges.

import { strict as assert } from "assert";
import invert from "ranges-invert";

assert.deepEqual(
  invert(
    [
      [3, 5],
      [5, 7],
    ],
    9 // string length needed to set the boundary
  ),
  [
    [0, 3],
    [7, 9],
  ]
);

See the package

§ ranges-is-index-within

Tells, is a given natural number index within any of the ranges. It's a wrapper on top of Array.prototype.find().

import { strict as assert } from "assert";
import isIndexWithin from "ranges-is-index-within";

assert.equal(
  isIndexWithin(8, [
    [1, 2],
    [5, 10],
  ]),
  true
);

assert.equal(
  isIndexWithin(12, [
    [1, 2],
    [5, 10],
  ]),
  false
);

See the package

§ ranges-iterate

It iterates all characters in a string, as if given ranges were already applied.

Sometimes certain operations on a string aren't really composable — sometimes we want to traverse the string as if ranges were applied, as if we already had the final result.

import { strict as assert } from "assert";
import iterate from "ranges-iterate";

// Ranges in the following example "punches out" a "hole" from `a` to `g`
// (included), replacing it with `xyz`. That's what gets iterated.

const gathered = [];

// a callback-based interface:
iterate(
  "abcdefghij",
  [[0, 7, "xyz"]],
  ({ i, val }) => {
    gathered.push(`i = ${i}; val = ${val}`);
  }
);

assert.deepEqual(gathered, [
  "i = 0; val = x",
  "i = 1; val = y",
  "i = 2; val = z",
  "i = 3; val = h",
  "i = 4; val = i",
  "i = 5; val = j",
]);

See the package

§ ranges-process-outside

Processes the string outside the given ranges. Each "gap" in the string between ranges will be fed into callback you supply — same like in Array.prototype.forEach().

This program makes the life easier because if you did it manually, you'd have to invert ranges and loop over each inverted chunk. Finally, you'd have to write unit tests of all that.

import { strict as assert } from "assert";
import processOutside from "ranges-process-outside";

const gathered = [];

// a callback interface:
processOutside(
  "abcdefghij",
  [
    [1, 5], // delete from "b" to "f"
  ],
  (fromIdx, toIdx, offsetValueCb) => {
    gathered.push(fromIdx);
  }
);

assert.deepEqual(gathered, [0, 5, 6, 7, 8, 9]);

See the package

§ Alternatives

§ magic-string

The magic-string opens in a new tab by Rich Harris, the Rollup creator.

It is an all-in-one program to perform operations on strings in a controllable manner. It's oriented at operations on code, and its produced sourcemaps are aimed at browsers.

Range libraries are best used for when you want to:

  • transfer string amendment instructions between programs
  • gather string amendment instructions and discard some, conditionally, or tweak them
  • when string processing is complex

magic-string is best used in programs similar to Rollup: you process code and generate sourcemaps for browsers.

In comparison:

magic-string method .overwrite - equivalent to ranges-push and ranges-apply.