Package Exports
- multi-integer-range
This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (multi-integer-range) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.
Readme
multi-integer-range
A small library that parses comma-delimited integer ranges (such as "1-3,8-10"
) and manipulates such range data. This type of data is commonly used to specify which lines to highlight or which pages to print.
Supported operations:
- Addition (e.g.,
1-2,6
+3-5
→1-6
) - Subtraction (e.g.,
1-10
-5-9
→1-4,10
) - Inclusion check (e.g.,
3,7-9
⊂1-10
) - Intersection (e.g.,
1-5
∩2-8
→2-5
) - Unbounded ranges (e.g.,
5-
, meaning "all integers ≥ 5") - Ranges including negative integers or zero
- ES6 iterator (
for ... of
, spread operator) - Array creation ("flatten")
Internal data are always sorted and normalized to the smallest possible representation.
Install
First, choose the right version you need. The API style has changed drastically in version 5. The new API is slightly more verbose but fully tree-shakable.
4.x | 5.x (alpha) | |
---|---|---|
API | Class-based | Function-based |
ES version | Downpiled to ES5 | ES2016 |
Module system | CommonJS | ESM (Tree-shakable❤️) |
Immutability | Mutable method chain | Pure functions only❤️ |
Supported runtime | Works even on IE | See below |
Supported runtime for version 5.x:
- Node >= 10: If you're consuming this module via modern preprocessors like Webpack or tsc
- Node >= 14: If you want to
import
the module natively - Deno: (haven't checked, should work via unpkg)
Version 5.x should be fine for most modern development environments, but if you feel you are not ready yet, feel free to use 4.x, which is stable and has no known major bugs.
Install via npm or yarn:
npm install multi-integer-range@5
Modern browsers can directly load this package as a standard ES module via CDN:
<script type="module">
import mr as * from 'https://unpkg.com/multi-integer-range@5/lib/fp.js';
console.log(mr.parse('7,6,5'));
</script>
Basic Example
The following is the documentation for version 5, which is still in alpha. Go to the docs for version 4
import * as mr from 'multi-integer-range';
const ranges1 = mr.parse('1-6,9-12'); // [[1, 6], [9, 12]]
const ranges2 = mr.parse('7-10,100'); // [[7, 10], [100, 100]]
const ranges3 = mr.normalize([1, 5, 6, [4, 2]]); // [[1, 6]]
const sum = mr.append(ranges1, ranges2); // [[1, 12], [100, 100]]
const diff = mr.subtract(ranges1, ranges2); // [[1, 6], [11, 12]]
const commonValues = mr.intersect(ranges1, ranges2); // [[9, 10]]
const str = mr.stringify(sum); // "1-12,100"
const bool = mr.has(range1, ranges3); // true
const isSame = mr.equals(range1, range2); // false
const array = mr.flatten(ranges3); // [1, 2, 3, 4, 5, 6]
const len = mr.length(ranges1); // 10
Creating a normalized MultiIntegerRange
The fundamental data structure of this module is a normalized read-only array of [min, max]
tuples, as shown in the following TypeScript definition. In other words, just an array of 2-element number arrays. Here, "normalized" means the range data is in the smallest possible representation and is sorted in ascending order.
type Range = readonly [min: number, max: number];
type MultiIntegerRange = readonly Range[];
type MIR = MultiIntegerRange; // short alias
// Examples of normalized MultiIntegerRanges
[[1, 3], [4, 4], [7, 10]]
[[-Infinity, -5], [-1, 0], [3, 3], [9, Infinity]]
[[-Infinity, Infinity]]
// These are NOT normalized MultiIntegerRanges
[[3, 1]] // min is larger than max
[[7, 9], [1, 4]] // not in the ascending order
[[1, 5], [3, 7]] // there is an overlap
[[1, 2], [3, 4]] // the two ranges can be combined to "1-4"
[[Infinity, Infinity]] // makes no sense
Most functions expect one or more normalized MultiIntegerRanges as shown above to work correctly. To produce a valid normalized MultiIntegerRange, you can use normalize()
or parse()
.
normalize(data?: number | (number | Range)[])
creates a normalized MultiIntegerRange from a single number or an unsorted array of numbers/Ranges. Importantly, normalize()
is the only function that can safely take an unsorted array. Do not pass un-normalized range data to other functions.
console.log(mr.normalize(10)); // [[10, 10]]
console.log(mr.normalize([3, 1, 2, 4, 5])); // [[1, 5]]
console.log(mr.normalize([5, [2, 0], 6])); // [[0, 2], [5, 6]]
console.log(mr.normalize([7, 7, 7, 7, 10])); // [[7, 7], [10, 10]]
console.log(mr.normalize()); // []
// Do not pass un-normalized data to functions other than normalize().
const unsorted = [[3, 1], [2, 8]];
const wrong = mr.length(unsorted); // DON'T! This won't work!
const correct = mr.length(mr.normalize(unsorted)); // 8
parse(data: string, options?: Options)
creates a normalized MultiIntegerRange from a string. The string parser is permissive and accepts space characters before/after comma/hyphens. It calls normalize()
under the hood, so the order is not important, and overlapped numbers are silently ignored.
console.log(mr.parse('1-3,10')); // [[1, 3], [10, 10]]
console.log(mr.parse('3,\t8-3,2,3,\n10, 9 - 7 ')); // [[2, 10]]
By default, the string parser does not try to parse unbounded ranges or negative integers. You need to pass an options
object to modify the parsing behavior. To avoid ambiguity, all negative integers must always be enclosed in parentheses (sorry for the wacky syntax, but it's always possible to make your custom parsing function if you prefer another syntax).
console.log(mr.parse('7-')); // throws a SyntaxError
console.log(mr.parse('7-', { parseUnbounded: true })); // [[7, Infinity]]
console.log(mr.parse('(-7)-(-1)', { parseNegative: true })); // [[-7, -1]]
console.log(
mr.parse('0-,(-6)-(-2),-(-100)', {
parseUnbounded: true,
parseNegative: true
})
); // [[-Infinity, -100], [-6, -2], [0, Infinity]]
API
All functions are "pure", and exported as named exports. They do not change the input data nor do they have any side effects. All MultiIntegerRange's returned by these functions are normalized. MIR
is just a short alias for MultiIntegerRange
(available in d.ts).
parse(data: string, options?: Options): MIR
Parses the given string.normalize(data?: number | (number | Range)[]): MIR
Normalizes the given number or the array of numbers/Ranges.append(a: MIR, b: MIR): MIR
Appends the two values.subtract(a: MIR, b: MIR): MIR
Subtractsb
froma
.intersect(a: MIR, b: MIR): MIR
Calculates the interesction, i.e., integers that belong to botha
andb
.has(a: MIR, b: MIR): boolean
Checks ifb
is a subset ofa
.length(data: MIR): number
Calculates how many numbers are effectively included in the given data (i.e., 5 for '3,5-7,9'). Returns Inifnity for unbounded ranges.equals(a: MIR, b: MIR): boolean
Checks ifa
andb
contains the same range data.isUnbounded(data: MIR): boolean
Returns true if the instance is unbounded.min(data: MIR): number | undefined
Returns the minimum integer. May return -Infinity.max(data: MIR): number | undefined
Returns the maxinum integer. May return Infinity.tail(data: MIR): MIR
Removes the minimum integer.init(data: MIR): MIR
Removes the maxinum integer.stringify(data: MIR): string
Returns the string respresentation of the given data (the opposite of parse()).flatten(data: MIR): number[]
Builds a flat array of integers. This may be slow and memory-consuming for large ranges such as '1-10000'.iterate(data: MIR): Iterable<number>
Returns an ES6 iterable object. See the description below.
Available options
that can be passed to parse()
:
parseNegative
(boolean, default = false): Enables parsing negative ranges (e.g.,(-10)-(-3)
).parseUnbounded
(boolean, default = false): Enables parsing unbounded ranges (e.g.,-5,10-
).
Iteration
Since MultiIntegerRange
is just an array of Range
s, if you naively iterate over it (e.g., in a for-of loop), you'll simply get each Range
tuple one by one. To iterate each integer contained in the MultiIntegerRange
instead, use iterate()
like so:
const ranges = mr.parse('2,5-7');
for (const page of mr.iterate(ranges)) {
console.log(page);
} // prints 2, 5, 6 and 7
// array spreading (alternative of flatten())
const arr1 = [...mr.iterate(ranges)]; //=> [2, 5, 6, 7]
const arr2 = Array.from(mr.iterate(ranges)); //=> [2, 5, 6, 7]
Tip
Combine Intersection and Unbounded Ranges
Intersection is especially useful to "trim" unbounded ranges.
const userInput = '-5,7-';
const pagesInMyDoc = [[1, 100]]; // '1-100'
const pagesToPrint = mr.intersect(
mr.parse(userInput, { parseUnbounded: true }),
pagesInMyDoc
);
console.log(mr.stringify(pagesToPrint)); // '1-5,7-100'
Legacy Classe-based API
For compatibility purposes, version 5.x still exports the MultiRange
class and multirange
function, which is mostly compatible with the 4.x API but uses the new functional API under the hood. See the 4.x documentation for the usage. The use of this compatibility layer is discouraged because it is not tree-shakable and has no performance merit. Use this only during migration.
Changelog
See CHANGELOG.md.
Caveats
Performance Considerations: This library works efficiently for large ranges
as long as they're mostly continuous (e.g., 1-10240000,20480000-50960000
). However, this library is not intended to be efficient with a heavily fragmented set of integers that are scarcely continuous (e.g., random 10000 integers between 1 to 1000000).
No Integer Type Checks: Make sure you are not passing floating-point number
s
to this library. For example, don't do normalize(3.14)
. For performance reasons, the library does not check if a passed number is an integer. Passing a float will result in unexpected and unrecoverable behavior.
Development
Building and Testing
npm install
npm run build
npm test
Bugs
Please report any bugs and suggestions using GitHub issues.
Author
Soichiro Miki (https://github.com/smikitky)
License
MIT