UNPKG

xduce

Version:

Composable algorithmic transformations library for JavaScript

280 lines (257 loc) 10.6 kB
/* * Copyright (c) 2017 Thomas Otterson * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in * the Software without restriction, including without limitation the rights to * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of * the Software, and to permit persons to whom the Software is furnished to do so, * subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /** * A whole bunch of utility functions. These are all used by the library itself in places, but many of them are * suitable for general use as well. ^ * @module util * @private */ /** * `Object`'s `toString` is explicitly used throughout because it could be redefined in any subtype of `Object`. * * @function toString * @private */ const toString = Object.prototype.toString; /** * **Determines whether a value is an array.** * * This function merely delegates to `Array.isArray`. It is provided for consistency in calling style only. * * @function isArray * @memberof module:xduce.util * * @param {*} x The value being tested to see if it is an array. * @return {boolean} Either `true` if the test value is an array or `false` if it is not. */ const isArray = Array.isArray; /** * **Determines whether a value is a function.** * * @memberof module:xduce.util * * @param {*} x The value being tested to see if it is a function. * @return {boolean} Either `true` if the test value is a function or `false` if it is not. */ function isFunction(x) { return toString.call(x) === '[object Function]'; } /** * **Determines whether a value is a plain object.** * * This function returns `false` if the value is any other sort of built-in object (such as an array or a string). It * also returns `false` for any object that is created by a constructor that is not `Object`'s constructor, meaning that * "instances" of custom "classes" will return `false`. Therefore it's only going to return `true` for literal objects * or those created with `Object.create()`. * * @memberof module:xduce.util * * @param {*} x The value being tested to see if it is a plain object. * @return {boolean} Either `true` if the test value is a plain object or `false` if it is not. */ function isObject(x) { // This check is true on all objects, but also on all objects created by custom constructors (which we don't want). // Note that in ES2015 and later, objects created by using `new` on a `class` will return false directly right here. if (toString.call(x) !== '[object Object]') { return false; } // The Object prototype itself passes, along with objects created without a prototype from Object.create(null); const proto = Object.getPrototypeOf(x); if (proto === null) { return true; } // Check to see whether the constructor of the tested object is the Object constructor, const ctor = Object.prototype.hasOwnProperty.call(proto, 'constructor') && proto.constructor; return typeof ctor === 'function' && ctor === Object; } /** * **Determines whether a value is a number.** * * This function will return `true` for any number literal or instance of `Number` except for `Infinity` or `NaN`. It * will return `false` for strings that happen to also be numbers; the value must be an actual `Number` instance or * number literal to return `true`. * * @memberof module:xduce.util * * @param {*} x The value being tested to see if it is a number. * @return {boolean} Either `true` if the test value is a finite number (not including string representations of * numbers) or `false` if it is not. */ function isNumber(x) { return toString.call(x) === '[object Number]' && isFinite(x); } /** * **Determines whether a value is a string.** * * Literal strings will return `true`, as will instances of the `String` object. * * @memberof module:xduce.util * * @param {*} x The value being tested to see if it is a string. * @return {boolean} Either `true` if the test value is a string or `false` if it is not. */ function isString(x) { return toString.call(x) === '[object String]'; } /** * **Returns the character at a particular index in a string, taking double-width * <abbr title="Basic Multilingual Plane">BMP</abbr> characters into account.** * * This is a BMP version of the standard JavaScript `string.charAt` function. The index is adjusted to account for * double-width characters in the input string, and if the resulting character is double-width, it will be returned as a * two-character string. The second half of these double-width characters don't get assigned an index at all, so it * works seemlessly between character widths. * * @function charAt * @memberof module:xduce.util.bmp * * @param {string} str The input string whose character at the given index is sought. * @param {number} index The index in the input string of the character being sought. * @return {string} The character at the given index in the provided string. If this character is a BMP character, * the full character will be returned as a two-character string. */ function bmpCharAt(str, index) { const s = str + ''; let i = index; const end = s.length; const pairs = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g; while (pairs.exec(s)) { const li = pairs.lastIndex; if (li - 2 < i) { i++; } else { break; } } if (i >= end || i < 0) { return ''; } let result = s.charAt(i); if (/[\uD800-\uDBFF]/.test(result) && /[\uDC00-\uDFFF]/.test(s.charAt(i + 1))) { result += s.charAt(i + 1); } return result; } /** * **Calculates the length of a string, taking double-width <abbr title="Basic Multilingual Plane">BMP</abbr> * characters into account.** * * Since this function takes double-width characters into account and the build in string `length` property does not, * it is entirely possible for this function to provide a different result than querying the same string's `length` * property. * * @function length * @memberof module:xduce.util.bmp * * @param {string} str The string whose length is being determined. * @return {number} The number of characters in the string, counting double-width BMP characters as single characters. */ function bmpLength(str) { const s = str + ''; const matches = s.match(/[\uD800-\uDBFF][\uDC00-\uDFFF]/g); const count = matches ? matches.length : 0; return s.length - count; } /** * **Creates an array of values between the specified ranges.** * * The actual range is `[start, end)`, meaning that the start value is a part of the array, but the end value is not. * * If only one parameter is supplied, it is taken to be `end`, and `start` is set to 0. If there is a third parameter, * it defines the distance between each successive element of the array; if this is missing, it's set to 1 if `start` is * less than `end` (an ascending range) or -1 if `end` is less than `start` (a descending range). * * If the step is going in the wrong direction - it's negative while `start` is less than `end`, or vice versa - then * the `start` value will be the only element in the returned array. This prevents the function from trying to * generate infinite ranges. * * ``` * const { range } = xduce.util; * * console.log(range(5)); // -> [0, 1, 2, 3, 4] * console.log(range(1, 5)); // -> [1, 2, 3, 4] * console.log(range(0, 5, 2)); // -> [0, 2, 4] * console.log(range(5, 0, -1)); // -> [5, 4, 3, 2, 1] * console.log(range(5, 0)); // -> [5, 4, 3, 2, 1] * console.log(range(0, 5, -2)); // -> [0] * ``` * * @memberof module:xduce.util * * @param {number} [start=0] The starting point, inclusive, of the array. This value will be the first value of the * array. * @param {number} end The ending point, exclusive, of the array. This value will *not* be a part of the array; it will * simply define the upper (or lower, if the array is descending) bound. * @param {number} [step=1|-1] The amount that each element of the array increases over the previous element. If this is * not present, it will default to `1` if `end` is greater than `start`, or `-1` if `start` is greater than `end`. * @return {number[]} An array starting at `start`, with each element increasing by `step` until it reaches the last * number before `end`. */ function range(start, end, step) { const [s, e] = end == null ? [0, start] : [start, end]; const t = step || (s > e ? -1 : 1); // This aborts the production if a bad step is given; i.e., if the step is going in a direction that does not lead // to the end. This prevents the function from never reaching the end value and trying to create an infinite range. if (Math.sign(t) !== Math.sign(e - s)) { return [s]; } const result = []; for (let i = s; t < 0 ? i > e : i < e; i += t) { result.push(i); } return result; } /** * **Creates a function that returns the opposite of the supplied predicate function.** * * The parameter lists of the input and output functions are exactly the same. The only difference is that the two * functions will return opposite results. If a non-predicate function is passed into this function, the resulting * function will still return a boolean that is the opposite of the truthiness of the original. * * ``` * const even = x => x % 2 === 0; * const odd = xduce.util.complement(even); * * console.log(even(2)); // -> true * console.log(odd(2)); // -> false * ``` * * @memberof module:xduce.util * * @param {function} fn A predicate function. * @return {function} A new function that takes the same arguments as the input function but returns the opposite * result. */ function complement(fn) { return (...args) => !fn(...args); } module.exports = { isArray, isObject, isFunction, isString, isNumber, bmpCharAt, bmpLength, range, complement };