Source rapidcontext/fn.mjs

1/*
2 * RapidContext <https://www.rapidcontext.com/>
3 * Copyright (c) 2007-2023 Per Cederberg. All rights reserved.
4 *
5 * This program is free software: you can redistribute it and/or
6 * modify it under the terms of the BSD license.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
11 *
12 * See the RapidContext LICENSE for more details.
13 */
14
15/**
16 * Provides predicate functions for type checking and equality.
17 * @namespace RapidContext.Fn
18 */
19
20/**
21 * Checks if a value is either `null` or `undefined`.
22 *
23 * @param {*} val the value to check
24 * @return {boolean} `true` on match, or `false` otherwise
25 * @name isNil
26 * @memberof RapidContext.Fn
27 */
28export function isNil(val) {
29    return val === null || val === undefined;
30}
31
32/**
33 * Checks if a value is `null` (but not `undefined`).
34 *
35 * @param {*} val the value to check
36 * @return {boolean} `true` on match, or `false` otherwise
37 * @name isNull
38 * @memberof RapidContext.Fn
39 */
40export function isNull(val) {
41    return val === null;
42}
43
44/**
45 * Checks if `typeof(val)` matches a string or an array element. Returns a
46 * bound function if only the first argument is specified.
47 *
48 * @param {string|Array} type the type name or array of type names
49 * @param {*} [val] the value to check
50 * @return {boolean|function} the match result, or a bound function
51 * @name isTypeOf
52 * @memberof RapidContext.Fn
53 */
54export function isTypeOf(type, val) {
55    if (arguments.length < 2) {
56        return isTypeOf.bind(null, ...arguments);
57    } else {
58        return Array.isArray(type) ? type.includes(typeof(val)) : typeof(val) === type;
59    }
60}
61
62/**
63 * Checks if a value is `undefined` (but not `null`).
64 *
65 * @param {*} val the value to check
66 * @return {boolean} `true` on match, or `false` otherwise
67 * @name isUndefined
68 * @memberof RapidContext.Fn
69 */
70export function isUndefined(val) {
71    return isTypeOf('undefined', val);
72}
73
74/**
75 * Checks if a value is a `boolean`` (i.e. `typeof(..) = "boolean"`).
76 *
77 * @param {*} val the value to check
78 * @return {boolean} `true` on match, or `false` otherwise
79 * @name isBoolean
80 * @memberof RapidContext.Fn
81 */
82export function isBoolean(val) {
83    return isTypeOf('boolean', val);
84}
85
86/**
87 * Checks if a value is a `function` (i.e. `typeof(..) = "function"`).
88 *
89 * @param {*} val the value to check
90 * @return {boolean} `true` on match, or `false` otherwise
91 * @name isFunction
92 * @memberof RapidContext.Fn
93 */
94export function isFunction(val) {
95    return isTypeOf('function', val);
96}
97
98/**
99 * Checks if a value is a `number` (i.e. `typeof(..) = "number"`).
100 *
101 * @param {*} val the value to check
102 * @return {boolean} `true` on match, or `false` otherwise
103 * @name isNumber
104 * @memberof RapidContext.Fn
105 */
106export function isNumber(val) {
107    return isTypeOf('number', val);
108}
109
110/**
111 * Checks if a value is a `bigint` (i.e. `typeof(..) = "bigint"`).
112 *
113 * @param {*} val the value to check
114 * @return {boolean} `true` on match, or `false` otherwise
115 * @name isBigInt
116 * @memberof RapidContext.Fn
117 */
118export function isBigInt(val) {
119    return isTypeOf('bigint', val);
120}
121
122/**
123 * Checks if a value is a `string` (i.e. `typeof(..) = "string"`).
124 *
125 * @param {*} val the value to check
126 * @return {boolean} `true` on match, or `false` otherwise
127 * @name isString
128 * @memberof RapidContext.Fn
129 */
130export function isString(val) {
131    return isTypeOf('string', val);
132}
133
134/**
135 * Checks if a value is a plain `Object` (and not based on another prototype).
136 *
137 * @param {*} val the value to check
138 * @return {boolean} `true` on match, or `false` otherwise
139 * @name isObject
140 * @memberof RapidContext.Fn
141 */
142export function isObject(val) {
143    // Note: Some built-in objects (e.g. Arguments) share prototype with Object
144    // Note: Object.prototype.toString() gives weird result in some other cases
145    let proto = Object.prototype;
146    let str = proto.toString;
147    return !!val && Object.getPrototypeOf(val) === proto && str.call(val) === '[object Object]';
148}
149
150/**
151 * Checks if a value is `instanceof` a constructor function. Returns a bound
152 * function if only the first argument is specified.
153 *
154 * @param {function} constructor the constructor function or class
155 * @param {*} val the value to check
156 * @return {boolean|function} the match result, or a bound function
157 * @name isInstanceOf
158 * @memberof RapidContext.Fn
159 */
160export function isInstanceOf(constructor, val) {
161    if (arguments.length < 2) {
162        return isInstanceOf.bind(null, ...arguments);
163    } else {
164        return val instanceof constructor;
165    }
166}
167
168/**
169 * Checks if a value is an instance of `Error`.
170 *
171 * @param {*} val the value to check
172 * @return {boolean} `true` on match, or `false` otherwise
173 * @name isError
174 * @memberof RapidContext.Fn
175 */
176export function isError(val) {
177    return isInstanceOf(Error, val);
178}
179
180/**
181 * Checks if a value is an instance of `Date`.
182 *
183 * @param {*} val the value to check
184 * @return {boolean} `true` on match, or `false` otherwise
185 * @name isDate
186 * @memberof RapidContext.Fn
187 */
188export function isDate(val) {
189    return isInstanceOf(Date, val);
190}
191
192/**
193 * Checks if a value is an instance of `RegExp`.
194 *
195 * @param {*} val the value to check
196 * @return {boolean} `true` on match, or `false` otherwise
197 * @name isRegExp
198 * @memberof RapidContext.Fn
199 */
200export function isRegExp(val) {
201    return isInstanceOf(RegExp, val);
202}
203
204/**
205 * Checks if a value is an instance of `Promise`.
206 *
207 * @param {*} val the value to check
208 * @return {boolean} `true` on match, or `false` otherwise
209 * @name isPromise
210 * @memberof RapidContext.Fn
211 */
212export function isPromise(val) {
213    return isInstanceOf(Promise, val);
214}
215
216const isArray = Array.isArray;
217
218/**
219 * Checks if a value is Array-like. This is an object with a numeric `length`
220 * property (but not a function).
221 *
222 * @param {*} val the value to check
223 * @return {boolean} `true` on match, or `false` otherwise
224 * @name isArrayLike
225 * @memberof RapidContext.Fn
226 */
227export function isArrayLike(val) {
228    return !!val && isNumber(val.length) && val.length >= 0 && !isFunction(val);
229}
230
231/**
232 * Checks if a value is Set-like. This is an object with a numeric `size`
233 * property, and `has()` and `keys()` methods.
234 *
235 * @param {*} val the value to check
236 * @return {boolean} `true` on match, or `false` otherwise
237 * @name isSetLike
238 * @memberof RapidContext.Fn
239 */
240export function isSetLike(val) {
241    return !!val && isNumber(val.size) && isFunction(val.has) && isFunction(val.keys);
242}
243
244/**
245 * Checks if a value is iterable (i.e. follows the iterable protocol). This is
246 * an object with a `Symbol.iterator` method returning the values.
247 *
248 * @param {*} val the value to check
249 * @return {boolean} `true` on match, or `false` otherwise
250 * @name isIterable
251 * @memberof RapidContext.Fn
252 */
253export function isIterable(val) {
254    return !!val && isFunction(val[Symbol.iterator]);
255}
256
257/**
258 * Checks if an object has an own property with a specified key. Returns a
259 * bound function if only the first argument is specified.
260 *
261 * @param {string} key the property key to check for
262 * @param {Object} [obj] the object to check
263 * @return {boolean|function} the match result, or a bound function
264 * @name hasProperty
265 * @memberof RapidContext.Fn
266 */
267export function hasProperty(key, obj) {
268    if (arguments.length < 2) {
269        return hasProperty.bind(null, ...arguments);
270    } else {
271        return !!obj && Object.prototype.hasOwnProperty.call(obj, key);
272    }
273}
274
275/**
276 * Checks if a value is set. Returns `false` for `null`, `undefined`, empty
277 * arrays, empty objects or empty strings. Boolean `false` and `0` will return
278 * `true`, as these are defined values.
279 *
280 * @param {*} val the value to check
281 * @return {boolean} `true` if the value is set, or `false` otherwise
282 * @name hasValue
283 * @memberof RapidContext.Fn
284 */
285export function hasValue(val) {
286    if (isNumber(val)) {
287        return !isNaN(val);
288    } else if (isArrayLike(val)) {
289        return val.length > 0;
290    } else if (isObject(val)) {
291        return Object.keys(val).length > 0;
292    } else if (isSetLike(val)) {
293        return val.size > 0;
294    } else {
295        return isBoolean(val) || isBigInt(val) || !!val;
296    }
297}
298
299/**
300 * Checks if one or more values are equal. Supports recursively comparing both
301 * arrays and objects, as well as standard values. Returns a bound function if
302 * only the first argument is specified. Also returns a bound function if one
303 * argument is a function.
304 *
305 * @param {...*} values the values to compare
306 * @return {boolean|function} the test result, or a bound function
307 * @name eq
308 * @memberof RapidContext.Fn
309 *
310 * @example
311 * eq(1, 1, 1, 3) //==> false
312 * eq({ a: 1, b: 2 }, { b: 2, a: 1 }) //==> true
313 *
314 * @example
315 * let isAbc = eq("abc");
316 * isAbc("abc") //==> true
317 *
318 * @example
319 * let hasValidA = eq(42, get("a"));
320 * hasValidA({ a: 42 }) //==> true
321 */
322export function eq(...values) {
323    function test() {
324        let valid = [];
325        for (let el of values) {
326            if (isFunction(el)) {
327                try {
328                    el = el.apply(null, arguments);
329                } catch (ignore) {
330                    return false;
331                }
332            }
333            if (valid.length > 0 && !isEq(valid[0], el)) {
334                return false;
335            }
336            valid.push(el);
337        }
338        return true;
339    }
340    function isEq(a, b) {
341        if (isArray(a) || isArray(b)) {
342            const aLen = isArray(a) && a.length;
343            const bLen = isArray(b) && b.length;
344            return aLen === bLen && a.every((el, i) => isEq(el, b[i]));
345        } else if (isObject(a) || isObject(b)) {
346            const aKeys = isObject(a) && Object.keys(a).sort();
347            const bKeys = isObject(b) && Object.keys(b).sort();
348            return isEq(aKeys, bKeys) && aKeys.every((k) => isEq(a[k], b[k]));
349        } else {
350            return a === b;
351        }
352    }
353    if (arguments.length < 2) {
354        return eq.bind(null, ...arguments);
355    } else if (values.some(isFunction)) {
356        return test;
357    } else {
358        return test();
359    }
360}
361
362export default {
363    isNil, isNull, isUndefined, isBoolean, isFunction, isNumber, isBigInt,
364    isString, isObject, isError, isDate, isRegExp, isPromise, isArrayLike,
365    isSetLike, isIterable, isTypeOf, isInstanceOf, hasProperty, hasValue, eq
366};
367