§ Quick Take

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);

§ Purpose

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.

§ API

This package exports a constructor, Ranges (with uppercase), which you call using new to create class instances:

const Ranges = require("ranges-push");
let ranges = new Ranges();
// or, with Optional Options Object:
let ranges = new Ranges({ limitToBeAddedWhitespace: true });
let ranges = new Ranges({ mergeType: 2 });

The ranges (with lowercase) is your classopens in a new tab which contains your ranges and gives you methods to get/set the values.

You can also provide an Optional Options Object when creating the class:

§ Optional Options Object

options object's keyType of its valueDefaultDescription
limitToBeAddedWhitespaceBooleanfalseIf set to true, if to-be-added string (3rd element in the range array) contains only whitespace (trim()s to empty string), replace it with: either line break \n (if there's at least one line break or \r in it) or with a single space (all other cases). Same applies when we have a string, surrounded by whitespace. That whitespace will be replaced with space or line break.
limitLinebreaksCountNumber1This is the number of maximum consecutive line breaks allowed in collapsed result. Practically, setting this to 2 would allow single blank lines in the output (for example, between paragraphs).
mergeTypeNumber1Default mode, 1 is concatenate clashing values, but alternative mode 2 is newer value overwrites older. See detailed explanation below

The Optional Options Object is validated by check-types-mini, so please behave: the settings' values have to match the API and settings object should not have any extra keys, not defined in the API. Naughtiness will cause error throws. We know, it's strict, but it prevents any API misconfigurations and helps to identify some errors early-on.

Here is the Optional Options Object in one place (in case you ever want to copy it):

{
limitToBeAddedWhitespace: false,
limitLinebreaksCount: 1,
mergeType: 1
}

You then interact with your newly-created ranges class by calling its methods:

§ opts.mergeType

When merging, ranges are sorted first. Then, pairs starting from the end of the sorted array are merged. Last two becomes one, last two becomes one and so on.

The challenge is, what to do with values to add, third range array's element.

For example,

const range1 = [1, 2, "a"];
const range2 = [1, 2, "b"];

The above ranges are "saying": replace characters in a string from index 1 to 2 with "a", replace characters in string from index 1 to 2 with "b".

Do we end up with "ab" or "b" or something else?

opts.mergeType let's you customise this behaviour:

  • In default mode, opts.mergeType === 1, clashing "to insert" values will always be concatenated ("ab" in example above)
  • In mode opts.mergeType === 2, if "to insert" values clash and starting indexes are the same — the latter value overrides the former ("b" in example above).

In all other aspects, opts.mergeType modes 1 and 2 are the same.

Practically, you activate the mode when you create the class:

const Ranges = require("ranges-push");
let rangesArr = new Ranges({ mergeType: 2 });

From there on, when you push to rangesArr, clashing values will be resolved according to "mergeType" rules.

§ ranges.add(from, to[, str])

alias - .push

Input argumentTypeObligatory?Description
deleteFromInteger, natural numberyesBeginning index of the slice
deleteToInteger, natural numberyesEnding index of the slice
strStringnoIf you want not only to delete but insert something, put that new string here

If you want only to insert and you don't want to delete anything, put both deleteFrom and deleteTo the same.

  • If the arguments are of a wrong type, it will throw and error.
  • Also, if you overload it, providing fourth, fifth input argument and so on if will throw too. It's for your safety because it might flag up something wrong happening in your code.

In essence, .add() behaves two ways:

  1. .add(1, 2), later .add(2, 3) will not create a new [2, 3] but extend [1, 2] into [1, 3]. This is to save time because we prevent bunch of connecting ranges from being recorded as separate ones.
  2. all other cases, if it's not an exact extension of a previous range, new range is added into the array. .add(2, 3), later .add(1, 2) will result in [ [2, 3], [1, 2] ]. The .current() method will clean it later. Read on...

Additionally, when .add merges two ranges and one completely overlaps another, the superset (larger) range will wipe out any "to-add" (third-argument) values of the subset (smaller) range(s).

You can use either .add or .push, both do the same thing.

§ ranges.current()

This method fetches the current state of your ranges array, sorts and merges it, then outputs it to you.

Result is either

  1. array of slice range arrays like:
[
// notice it's an array of arrays
[10, 20, " insert this string after deleting range between indexes 10 & 20"][
(30, 50)
],
[51, 55],
];
  1. or null if it's still empty and nothing has been added since.

.current() will do the sorting first by deleteFrom (first element), then, sorting by deleteTo (second element), then, it will merge any ranges that overlap.

[[4, 5], [1, 2]] => [[1, 2], [4, 5]] // no overlap, so just sorted by 1st element
[[2, 5], [2, 3], [1, 10]] => [[1, 10]] // there was an overlap, so ranges were merged

In theory, since .current() does not mutate our ranges array in the memory, you could add more ranges and call .current() again, this time possibly with a slightly different result. However, be aware that merging will lose some of the data in the ranges.

Imagine: [ [10, 20, 'aaa'], [10, 15, bbb]] was merged by .current, and became [ [10, 20, 'bbbaaa'] ]. Now if you use this range in ranges-apply to amend the string, but then later discover that you left out the range [12, 17, ccc], that is, you wanted to delete between indexes 12 and 17, and then insert ccc, you'll be in trouble. Since you amended your string, you can't "stick in" ccc between original bbb and aaa — your desired place to add ccc, at index 17 has been "merged" by bbb and aaa.

Conclusion: complete all your operations, add()-ing ranges. Then, fetch your master ranges array once, using .current and feed it into ranges-apply. At this point don't do any more add()ing, or if you really want that, process the ranges you've got using ranges-apply, wipe() everything and start add()ing again.

§ ranges.wipe()

Sets your ranges array to null. Right after that ranges.current() will yield null. You can then start add-ing again, from scratch.

§ ranges.replace(newRanges)

If you have a new set of ranges and you want to replace existing set, instead of using ranges.wipe() and then iterating through all new ranges and adding them one-by-one, you can simple replace everything using ranges.replace(). For example:

const oldRanges = new Ranges();
oldRanges.add(1, 2, "a");
oldRanges.add(3, 4, "b");
oldRanges.add(9, 10);
console.log(oldRanges.current());
// => [[1, 2, "a"], [3, 4, "b"], [9, 10]]

// now replace them with new ranges:
oldRanges.replace([[6, 8, "zzz"]]);
console.log(oldRanges.current());
// => [[6, 8, "zzz"]]

§ ranges.last()

Outputs:

  1. the last ranges' array from the ranges array, for example:
[51, 55];
  1. Or, if there's nothing in the ranges array yet, null.

PSST. Later, feed your ranges array into ranges-apply to delete/replace all those ranges in your string.

§ In our case

Originally this library was part of email-comb, but we tore it off and placed into a separate (this) library when we needed the same function in html-img-alt. Since then, Detergent also uses it, so its unit test wouldn't take an hour, calculating all possible combinations of the options, while input string is mutated again and again in the for a loop.

§ Licence

MITopens in a new tab

Copyright © 2010–2020 Roy Revelt and other contributors

Related packages:

📦 ranges-apply 3.2.3
Take an array of string index ranges, delete/replace the string according to them
📦 ranges-merge 5.0.3
Merge and sort string index ranges
📦 ranges-sort 3.13.3
Sort string index ranges
📦 string-range-expander 1.11.11
Expands string index ranges within whitespace boundaries until letters are met
📦 ranges-is-index-within 1.15.2
Checks if index is within any of the given string index ranges
📦 ranges-process-outside 2.2.35
Iterate string considering ranges, as if they were already applied
📦 ranges-iterate 1.1.48
Iterate a string and any changes within given string index ranges