Skip to content

Commit 773bbed

Browse files
committed
Merge branch 'feature-open-ranges' into dev
2 parents 62c387a + 7ed7283 commit 773bbed

File tree

3 files changed

+205
-20
lines changed

3 files changed

+205
-20
lines changed

README.md

+46-3
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
[![Build Status](https://travis-ci.org/smikitky/node-multi-integer-range.svg?branch=master)](https://travis-ci.org/smikitky/node-multi-integer-range)
44

55
A small library which parses and manipulates comma-delimited integer ranges (such as "1-3,8-10").
6-
76
Such strings are typically used in print dialogs to indicate which pages to print.
87

98
Supported operations include:
@@ -12,8 +11,9 @@ Supported operations include:
1211
- Subtraction (e.g., `1-10` - `5-9` => `1-4,10`)
1312
- Inclusion check (e.g., `3,7-9` is in `1-10`)
1413
- Intersection (e.g., `1-5``2-8` => `2-5`)
14+
- Unbounded ranges (e.g., `5-` to mean "all integers >= 5")
1515
- Iteration using `for ... of`
16-
- Array creation ("flatten")
16+
- Array creation (a.k.a. "flatten")
1717

1818
Internal data are always *sorted and normalized* to the smallest possible
1919
representation.
@@ -102,6 +102,7 @@ To get the copy of the instance, use `clone()`, or alternatively the copy constr
102102
- `length(): number` Calculates how many numbers are effectively included in this instance. (ie, 5 for '3,5-7,9')
103103
- `segmentLength(): number` Returns the number of range segments (ie, 3 for '3,5-7,9' and 0 for an empty range)
104104
- `equals(cmp: Initializer): boolean` Checks if two MultiRange data are identical.
105+
- `isUnbounded(): boolean` Returns if the instance is unbounded.
105106
- `toString(): string` Returns the string respresentation of this MultiRange.
106107
- `getRanges(): [number, number][]` Exports the whole range data as an array of [number, number] arrays.
107108
- `toArray(): number[]` Builds an array of integer which holds all integers in this MultiRange. Note that this may be slow and memory-consuming for large ranges such as '1-10000'.
@@ -114,17 +115,59 @@ The following methods are deprecated and may be removed in future releases:
114115
- `hasRange(min: number, max: number): boolean` Use `has([[min, max]])` instead.
115116
- `isContinuous(): boolean` Use `segmentLength() === 1` instead.
116117

118+
119+
### Unbounded ranges
120+
121+
Starting from version 2.1, you can use unbounded (or infinite) ranges,
122+
which look like this:
123+
124+
```js
125+
// using the string parser...
126+
var unbounded1 = new MultiRange('5-'); // all integers >= 5
127+
var unbounded2 = new MultiRange('-3'); // all integers <= 3
128+
var unbounded3 = new MultiRange('-'); // all integers
129+
130+
// or programmatically, using the JavaScript constant `Infinity`...
131+
var unbounded4 = new MultiRange([[5, Infinity]]); // all integers >= 5
132+
var unbounded5 = new MultiRange([[-Infinity, 3]]); // all integers <= 3
133+
var unbounded6 = new MultiRange([[-Infinity, Infinity]]); // all integers
134+
```
135+
136+
The manipulation methods work just as expected with unbounded ranges:
137+
138+
```js
139+
console.log(multirange('5-10,15-').append('0,11-14') + ''); // '0,5-'
140+
console.log(multirange('-').subtract('3-5,9') + ''); // '-2,6-8,10-'
141+
console.log(multirange('-5,10-').has('-3,20')); // true
142+
143+
// intersection is especially useful to "trim" any unbounded ranges:
144+
var userInput = '-10,15-20,90-';
145+
var pagesInMyDoc = '1-100';
146+
var pagesToPrint = multirange(userInput).intersect(pagesInMyDoc);
147+
console.log(pagesToPrint); // prints '1-10,15-20,90-100'
148+
```
149+
150+
Unbounded ranges cannot be iterated over, and you cannot call `#toArray()`
151+
for the obvious reason. Calling `#length()` for unbounded ranges will return `Infinity`.
152+
117153
### Ranges Containing Zero or Negative Integers
118154

119155
You can handle ranges containing zero or negative integers.
120156
To pass negative integers to the string parser, always contain them in parentheses.
157+
Otherwise, it will be parsed as an unbounded range.
121158

122159
```js
123-
var mr1 = new MultiRange('(-5),(-1)-0');
160+
var mr1 = new MultiRange('(-5),(-1)-0'); // -5, -1 and 0
124161
mr1.append([[-4, -2]]); // -4 to -2
125162
console.log(mr1 + ''); // prints '(-5)-0'
126163
```
127164

165+
Again, note that passing `-5` to the string parser means
166+
"all integers <=5 (including 0 and all negative integers)"
167+
rather than "minus five".
168+
If you are only interested in positive numbers, you can use
169+
`.intersect('0-')` to drop all negative integers.
170+
128171
### Iteration
129172

130173
**ES6 iterator**: If `Symbol.iterator` is defined in the runtime,

multi-integer-range.ts

+51-10
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export class MultiRange {
2020
if (typeof data === 'string') {
2121
this.parseString(data);
2222
} else if (typeof data === 'number') {
23-
this.ranges.push([data, data]);
23+
this.appendRange(data, data);
2424
} else if (data instanceof MultiRange) {
2525
this.ranges = data.getRanges();
2626
} else if (isArray(data)) {
@@ -53,15 +53,18 @@ export class MultiRange {
5353
}
5454

5555
const s = data.replace(/\s/g, '');
56-
if (s.length === 0) return;
56+
if (!s.length) return;
57+
let match;
5758
for (let r of s.split(',')) {
58-
let match = r.match(/^(\d+|\(\-?\d+\))(\-(\d+|\(\-?\d+\)))?$/);
59-
if (match) {
60-
const min = toInt(match[1]);
61-
const max = typeof match[3] !== 'undefined' ? toInt(match[3]) : min;
59+
if (match = r.match(/^(\d+|\(\-?\d+\))$/)) {
60+
const val = toInt(match[1]);
61+
this.appendRange(val, val);
62+
} else if (match = r.match(/^(\d+|\(\-?\d+\))?\-(\d+|\(\-?\d+\))?$/)) {
63+
const min = match[1] === undefined ? -Infinity : toInt(match[1]);
64+
const max = match[2] === undefined ? +Infinity : toInt(match[2]);
6265
this.appendRange(min, max);
6366
} else {
64-
throw new SyntaxError('Invalid input');
67+
throw new SyntaxError('Invalid input: ' + data);
6568
}
6669
};
6770
}
@@ -99,6 +102,11 @@ export class MultiRange {
99102
if (newRange[0] > newRange[1]) {
100103
newRange = [newRange[1], newRange[0]];
101104
}
105+
if (newRange[0] === Infinity && newRange[1] === Infinity ||
106+
newRange[0] === -Infinity && newRange[1] === -Infinity
107+
) {
108+
throw new RangeError('Infinity can be used only within an unbounded range');
109+
}
102110
const overlap = this.findOverlap(newRange);
103111
this.ranges.splice(overlap.lo, overlap.count, overlap.union);
104112
return this;
@@ -315,8 +323,10 @@ export class MultiRange {
315323
* Calculates how many numbers are effectively included in this instance.
316324
* (i.e. '1-10,51-60,90' returns 21)
317325
* @return The number of integer values in this instance.
326+
* Returns `Infinity` for unbounded ranges.
318327
*/
319328
public length(): number {
329+
if (this.isUnbounded()) return Infinity;
320330
let result = 0;
321331
for (let r of this.ranges) result += r[1] - r[0] + 1;
322332
return result;
@@ -344,6 +354,18 @@ export class MultiRange {
344354
}
345355
}
346356

357+
/**
358+
* Checks if the current instance is unbounded (i.e., infinite).
359+
*/
360+
public isUnbounded(): boolean
361+
{
362+
return (
363+
this.ranges.length > 0
364+
&& (this.ranges[0][0] === -Infinity ||
365+
this.ranges[this.ranges.length-1][1] === Infinity)
366+
);
367+
}
368+
347369
/**
348370
* Returns the string respresentation of this MultiRange.
349371
*/
@@ -352,9 +374,22 @@ export class MultiRange {
352374
function wrap(i: number): string {
353375
return (i >= 0 ? String(i) : `(${i})`);
354376
}
355-
const ranges = [];
356-
for (let r of this.ranges)
357-
ranges.push(r[0] == r[1] ? wrap(r[0]) : wrap(r[0]) + '-' + wrap(r[1]));
377+
const ranges: string[] = [];
378+
for (let r of this.ranges) {
379+
if (r[0] === -Infinity) {
380+
if (r[1] === Infinity) {
381+
ranges.push('-');
382+
} else {
383+
ranges.push(`-${wrap(r[1])}`);
384+
}
385+
} else if (r[1] === Infinity) {
386+
ranges.push(`${wrap(r[0])}-`);
387+
} else if (r[0] == r[1]) {
388+
ranges.push(wrap(r[0]));
389+
} else {
390+
ranges.push(`${wrap(r[0])}-${wrap(r[1])}`);
391+
}
392+
}
358393
return ranges.join(',');
359394
}
360395

@@ -364,6 +399,9 @@ export class MultiRange {
364399
*/
365400
public toArray(): number[]
366401
{
402+
if (this.isUnbounded()) {
403+
throw new RangeError('You cannot build an array from an unbounded range');
404+
}
367405
const result = new Array(this.length());
368406
let idx = 0;
369407
for (let r of this.ranges) {
@@ -379,6 +417,9 @@ export class MultiRange {
379417
*/
380418
public getIterator(): { next: () => { done: boolean, value: number }}
381419
{
420+
if (this.isUnbounded()) {
421+
throw new RangeError('Unbounded ranges cannot be iterated over');
422+
}
382423
let i = 0,
383424
curRange: Range = this.ranges[i],
384425
j = curRange ? curRange[0] : undefined;

0 commit comments

Comments
 (0)