Giter Club home page Giter Club logo

node-multi-integer-range's Introduction

multi-integer-range

Build Status Coverage Status npm version

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.

Key features:

  • Addition (aka union, e.g., 1-2,6 + 3-51-6)
  • Subtraction (e.g., 1-105-91-4,10)
  • Inclusion check (e.g., 3,7-91-10)
  • Intersection (e.g., 1-52-82-5)
  • Unbounded ranges (aka infinite ranges, e.g., 5-, meaning "all integers ≥ 5")
  • Ranges including negative integers or zero
  • ES6 iterator (for ... of, spread operator)
  • Array building ("flatten")

The range data are always sorted and normalized to the smallest possible representation.


🚨 Note: The following README is for the 5.x release, whose API has changed drastically. For the docs of the 4.x release, see this.

Install

Install via npm or yarn:

npm install multi-integer-range

Version 5 is a hybrid package; it provides both a CommonJS version and an ES Module version, built from the same TypeScript source. Bundlers such as Webpack can automatically pick the ESM version and perform tree-shaking. This package has no external dependencies nor does it use any Node-specific API.

🚨 The API style has changed drastically in version 5. The new API is slightly more verbose, but is simpler and tree-shakable 🌲. For example, if you don't use the default parser, your bundle will not include it. See the CHANGELOG and the docs for version 4.

Basic Example

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(ranges1, ranges3); // true
const isSame = mr.equals(ranges1, ranges2); // false
const array = mr.flatten(diff); // [1, 2, 3, 4, 5, 6, 11, 12]
const len = mr.length(ranges1); // 10

Creating a normalized MultiIntegerRange

The fundamental data structure of this package is a normalized array of [min, max] tuples, as shown below. Here, 'normalized' means the range data is in the smallest possible representation and is sorted in ascending order. You can denote an unbounded (aka infinite) range using the JavaScript constant Infinity.

type Range = readonly [min: number, max: number];
type MultiIntegerRange = readonly Range[];

// Examples of normalized MultiIntegerRanges
[[1, 3], [5, 6], [9, 12]] // 1-3,5-6,9-12
[[-Infinity, 4], [7, 7], [10, Infinity]] // -4,7,10-
[[-Infinity, Infinity]] // all integers
[] // empty

// These are NOT normalized. Don't pass them to append() and such!
[[3, 1]] // min is larger than max
[[7, 9], [1, 4]] // not in the ascending order
[[1, 5], [3, 7]] // there is an overlap of ranges
[[1, 2], [3, 4]] // the two ranges can be combined to "1-4"
[[Infinity, Infinity]] // makes no sense

Most functions take one or two normalized MultiIntegerRanges as shown above to work correctly. To produce a valid normalized MultiIntegerRange, you can use normalize(), parse() or initialize(). You can write a normalized MultiIntgerRange by hand as shown above, too.

normalize(data?: number | (number | Range)[]) creates a normalized MultiIntegerRange from a single integer or an unsorted array of integers/Ranges. This and initialize are the only functions that can safely take an unsorted array. Do not pass unnormalized 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, 4])); // [[0, 2], [4, 6]]
console.log(mr.normalize([7, 7, 10, 7, 7])); // [[7, 7], [10, 10]]
console.log(mr.normalize()); // []

// Do not directly pass an unnormalized array
// to functions other than normalize().
const unsorted = [[3, 1], [2, 8]];
const wrong = mr.length(unsorted); // 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. If you don't like the default parse(), you can always create and use your custom parsing function instead, as long as it returns a normalized MultiIntegerRange.

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 Reference

See api-reference.md.

Tips

Iteration

Since a MultiIntegerRange is just an array of Ranges, 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 ranges) {
  console.log(page);
} // prints 2 items: [2, 2] and [5, 7]

for (const page of mr.iterate(ranges)) {
  console.log(page);
} // prints 4 items: 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]

Combine Intersection and Unbounded Ranges

Intersection is especially useful to "trim" unbounded ranges.

const userInput = '-5,15-';
const pagesInMyDoc = [[1, 20]]; // 1-20
const pagesToPrint = mr.intersect(
  mr.parse(userInput, { parseUnbounded: true }),
  pagesInMyDoc
); // [[1, 5], [15, 20]]
for (const page of mr.iterate(pagesToPrint)) await printPage(page);

Legacy Classe-based API

For compatibility purposes, version 5 exports the MultiRange class and multirange function, which is mostly compatible with the 4.x API but has been rewritten to use 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. These may be removed in the future.

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 numbers 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.

Comparison with Similar Libraries

range-parser specializes in parsing range requests in HTTP headers as defined in RFC 7233, and it behaves in a way that is usually inappropriate for other purposes. For example, '-5' means "last 5 bytes".

parse-numeric-range is fine for small ranges, but it always builds a "flat" array, which makes it very inefficient for large ranges such as byte ranges. Also, whether you like it or not, it handles overlapping or descending ranges as-is, without normalization. For example, '4-2,1-3' results in [4, 3, 2, 1, 2, 3].

multi-integer-range is a general-purpose library for handling this type of data structure. It has a default parser that is intuitive enough for many purposes, but you can also use a custom parser. Its real value lies in its ability to treat normalized ranges as intermediate forms, allowing for a variety of mathematical operations. See the API reference.

Input multi-integer-range range-parser parse-numeric-range
'1-3' [[1, 3]] [{ start: 1, end: 3 }] [1, 2, 3]
'1-1000' [[1, 1000]] [{ start: 1, end: 1000 }] [1, 2, ..., 999, 1000 ] ⚠️
'5-1' [[1, 5]] (error) [5, 4, 3, 2, 1]
'4-2,1-3' [[1, 4]] [{ start: 1, end: 3 }] ⚠️1 [4, 3, 2, 1, 2, 3]
'-5' [[-Infinity, 5]] 2 [{ start: 9995, end: 9999 }] 3 [-5]
'5-' [[5, Infinity]] 2 [{ start: 5, end: 9999 }] 3 []

1: With combine option. 2: With parseUnbounded option. 3: When size is 10000.

Development

To test:

npm ci
npm test

To generate CJS and ESM builds:

npm ci
npm run build

Please report bugs and suggestions using GitHub issues.

Changelog

See CHANGELOG.md.

Author

Soichiro Miki (https://github.com/smikitky)

License

MIT

node-multi-integer-range's People

Contributors

dependabot[bot] avatar smikitky avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar

node-multi-integer-range's Issues

TypeScript compatibility notes

Due to the recent updates of both TypeScript and multi-integer-range, those who use TypeScript to build your own project may run into compatibility issues.

Here's the summary:

  • If you are not using TypeScript in your own project, forget this issue. This library works fine with vanilla JS, Babel, CoffeeScript, etc. (Otherwise please let me know!)
  • Regardless of whether you want to enable --strictNullChecks, you need to update your TypeScript to 2.0.x before upgrading this library to 3.0.x. If you want to stay in TypeScript <= 1.8.x for a little longer, feel free to keep using multi-integer-range 2.0.x, which has no known major bugs.
  • Regardless of the TypeScript version, do not use ES6 iterator (eg, for-of, spread operator) if your project is --target=es5. Down-level transformation works only with plain arrays.
  • Regardless of the TypeScript version, add the snippet in README somewhere in your project if your project is --target=es6 and uses ES6 iterator.

Ranges containing 0 and negative integers

Extend this library to support ranges containing zero and negative integers.

Actually the current manipulation methods probably handle negative integers just fine, so the main task is changing the string parser and toString().

When passing a string to the parser, negative integers always have to be contained in parentheses, e.g., (-10),(-3)-(-1),0,2-4. This syntax is obviously cumbersome, but '-5' will eventually be interpreted not as "minus 5" but as an open-ended range that means "all integers <= 5". See #2

BigInt support

Steps

  • Allow raw bigint values as input (new MultiRange([[555n, 999n]]))
  • Add an option to parse strings as bigint
    new MultiRange('999999999999999999', { bigInt: true });
    // Should throw RangeError without bigInt option
  • Update type definitions after TypeScript officially supports BigInt

We will not do automatic type conversions to/from plain numbers. You should use use either number or bigint exclusively within a single instance of MultiRange.

Create options to turn off negative/unbound ranges on parsing

A MultiRange object can hold negative and infinity values by default, but this is not always the intended behavior. We'll introduce an optional parameter to explicitly turn off negative/unbounded ranges on parsing, like so:

// Raises a SyntaxError because negative ranges are turned off
new MultiRange('(-3)-5', { negative: false });

// Raises a SyntaxError because unbounded ranges are turned off
new MultiRange('3-', { unbounded: false });

The passed option will take effect on subsequent chained methods:

// Still raises a SyntaxError
new MultiRange('1-5', { unbounded: false }).append(7).append('10-');

// Even after cloning
new MultiRange('1-5', { unbounded: false }).clone().append('10-');

For performance reasons, this will not take effect when you modify values programmatically (i.e. not using the string parser)

// Does not throw an error
multirange('1-5', { unbounded: false }).add([[10, Infinity]]);

// Does not throw an error
multirange('1-5', { negative: false }).add(-3);

Ideas for v5

  • Export as a pure ES module (*.mjs, Node >= 14)
  • Drop support for non-ES5-compatible runtimes (i.e., assumes symbols, Array.isArray, etc)
  • Stop using a class in favor of function-style API (in the spirit of date-fns, lodash/fp, etc)
  • (Option) Adopt immutability?
  • Support currying to support "chaining"

Before:

import MultiRange from 'multi-integer-range'; // 4.x
const str = new MultiRange('1-3,8-10')
  .append(5)
  .subtract('10-')
  .toString(); // 1-3,5,8-9

After (using pipeline operators and currying):

import * as mr from 'multi-integer-range'; // 5.x
const str = mr.create('1-3,8-10')
  |> mr.append(5)
  |> mr.subtract('10-')
  |> mr.stringify; // 1-3,5,8-9

Main pros and cons:

  • 👍 Smaller bundle size (with tree-shaking)
  • 👎 Loss of encapsulation (which was enforced only by TypeScript, anyway)
  • 👎 A bit messy code when pipeline operators aren't available
  • 👎 Immutability results in a small performance loss

Included d.ts file doesn't work with `for..of` loop in TypeScript ES6 mode

If you are using this library in your TypeScript project with --target ES6, and use ES6 iterator (for ... of loops, spread operator), then you will encounter a compile-time error. The compiler will say "error TS2488: Type must have a 'Symbol.iterator' method that returns an iterator." The compiled JS file works fine.

import { MultiRange } from 'multi-integer-range';
for (let i of new MultiRange('1-5')) console.log(i); // compile (but not runtime) error!
const arr = [...(new MultiRange('1-5'))]; // compile (but not runtime) error!

This is because the *.d.ts file included in the package is ES5-compatible, and does not have any declarations for ES6 iterators and symbols. To work around this, please add the following somewhere in your --target ES6 project:

declare module "multi-integer-range" {
    interface MultiRange {
        [Symbol.iterator](): Iterator<number>;
    }
}

If you're using TypeScript with --target ES5 flag, don't use for...of loops with MultiRange anyway; TypeScript only supports for...of loops for plain old arrays in ES5 mode.

Add support for intersection

It would be great if there was the ability to get a union and an intersection of two ranges. Something like:

let range1 = new MultiRange('1-2');
let range2 = new MultiRange('2-3,5');
let union = range1.union(range2);
let intersect = range1.intersect(range2);
console.log(union.equals('1-3,5'));
console.log(intersect.equals('2'));

Thank you. Having a numerical range is very helpful with something I'm working on and I'm so glad I don't have to reinvent the wheel.

String parser should throw RangeError for huge integers

String parser should properly throw an error for strings like 9999999999999999999999999. All numbers should be between Number.MIN_SAFE_INTEGER and Number.MAX_SAFE_INTEGER, or we will lose precision.

These constants are not defined in IE, so we will include these numbers in the source for now.

Copy constructor does not copy parse options

const { multirange } = require('multi-integer-range');

const a = multirange([1,5,7], { parseUnbounded: true });
const b = multirange(a);
console.log(b);
  • Expected: b.options.parseUnbounded === true
  • Actual: b.options.parseUnbounded === false

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.