From bca0629d91d2da765077c69fab6303807fc61f55 Mon Sep 17 00:00:00 2001 From: Simon Blackwell Date: Tue, 12 Feb 2019 07:27:08 -0500 Subject: [PATCH] All functions now have basic documentation. --- README.md | 254 ++++++++++++++++++--------- index.js | 475 ++++++++++++++++++++++++++++++++++++++++++-------- package.json | 2 +- test/index.js | 20 ++- 4 files changed, 594 insertions(+), 157 deletions(-) diff --git a/README.md b/README.md index 8ba64f6..63e86cf 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ JavaScript Object Query Language Representation - Funny it's mostly JSON. -JOQULAR is a query language specification. Although there is a reference implementation with this site others are free to write their own drivers. +JOQULAR is a query language specification. Although there is a reference implementation with this site, others are free to write their own drivers. Like SQL, JOQULAR allows you to mutate the returned records to meet you needs. @@ -24,8 +24,8 @@ const [,...] = await JOQULAR.query(,,...); ```javascript const pattern = {name:{$eq:"joe"}}, - [matched1] = await JOQULAR.match(pattern,{name:"joe",age:27}), // will resolve to the object with name "joe" - [matched2] = await JOQULAR.match(pattern,{name:"jane",age:27}); // will resolve to undefined + [matched1] = await JOQULAR.match(pattern,{name:"joe",age:27}), // will resolve to the object with name "joe" + [matched2] = await JOQULAR.match(pattern,{name:"jane",age:27}); // will resolve to undefined ``` JOQULAR queries can cause mutations. If an object is mutated during a query, a copy is returned. If no mutations occur, the original is returned. If you always want a copy, then do something to force a slight mutation, e.g. add a hidden property with @@ -37,7 +37,7 @@ For now see the [Medium article](https://medium.com/@anywhichway/joqular-high-po ## Predicates and Functions -All 92 predicates and functions or their aliases could actually be defined inline; however, functiona that are difficult to implement or used frequently are defined as part of JOQULAR. +All 97 predicates and functions or their aliases could actually be defined in-line; however, functions that are difficult to implement or used frequently are defined as part of JOQULAR. Additionally, by not using in-line functions, most JOQULAR patterns can be transmitted over the wire. New functions can be added in as little as one line of code, e.g. @@ -59,7 +59,9 @@ $ - `{:{$: (value,property,object) => ...}` * Calls a function with the property value, name, and object. The function can change the object. Matching continues if the function returns `true`. -$and - `{$and: }` +$and - `{:{$and: Array [,...]||}}` + +Returns `true` and matching continues if the value of the target `` satisfies all of the JOQULAR patterns in the `Array` or a nested logical pattern, e.g. `{$or: {$and: ...}`; otherwise, matching fails. $as - `{:{$as: string as}}` @@ -75,7 +77,7 @@ $avga - `{: {$avg: null||string as}}` $between - `{: {$between: [number hi,number lo,boolean inclusive]}}` -* Returns `true` in the value of `` on the target is between `hi` and `lo`, optionally inclusive of the boundaries. +* Returns `true` and mathcing continues if the value of `` on the target is between `hi` and `lo`, optionally inclusive of the boundaries; otherwise, matching fails. The value of the target `` can be a Date in addition to a `number` or a `string`. The function is polymorphic. The function is polymorphic. If the method `between` is available on the value of the target ``, it is called with `hi` and `lo` as arguments. Hence, it can be used for geometric objects, 3D space, or other custom classes. $compute - `{: {$compute (value,key,object) => ... || Array [function f,string as]}}` @@ -83,7 +85,7 @@ $compute - `{: {$compute (value,key,object) => ... || Array [function $count - `{: {$count: number count}}` -* Returns true if the value of `` equals `count` based on a full evaluation of the underlying iterable on the target ignoring `undefined`. +* Returns `true` if the value of `` equals `count` based on a full evaluation of the underlying iterable on the target ignoring `undefined`. $counta - `{: {$counta: number count}}` @@ -91,11 +93,11 @@ $counta - `{: {$counta: number count}}` $date - `{: {$date: number dayOfMonth}}` -* Returns true if the target `` has a Date value where `getDate() === dayOfMonth`. +* If the target `` has a Date value where `getDate() === dayOfMonth`, matching continues; otherwise, matching fails. $day - `{: {$day: number dayOfWeek}}` -* Returns true if the target `` has a Date value where `getDay() === dayOfWeek`. +* If the target `` has a Date value where `getDay() === dayOfWeek`, matching continues; otherwise, matching fails. $default - `{: {$default: any value}}` @@ -107,129 +109,167 @@ $define - `{: {$define: {enumerable,configurable,writable[,value]}}` $descendant - `{: {$descendant: }}` -* Returns true if any descendant of the property on the target matches the ``. +* Returns `true` and matching continues if any descendant of the property on the target matches the ``; otherwise, matching fails. $disjoint - `{: {$disjoint: Array array}}` -* Returns true if the array on the target `` is disjoint (has no values in common) with `array`. +* Returns `true` and matching continues if the array on the target `` is disjoint (has no values in common) with `array`; otherwise, matching fails. + +$disjunction - `{: {$disjunction: Array array}}` + +* Sets the `` on the target to the disjunction of the array on the target with `array`. If the property does not contain an array, matching fails. If `array` has an `as` and additional property, it is added to hold the disjunction. To set the `as` property on an array, pass in this self evaluating function: `((array, as) => { array.as = as; return array; })()`. $echoes - `{: {$echoes: string value}}` -* Returns the result of `soundex()===soundex(value)`. +* If `soundex()===soundex(value)`, matching continues; otherwise, matching fails. $eeq - `{: {$eeq: string||number||boolean||object value}}` -* Returns the result of ` === value`. +* If ` === value`, matching continues; otherwise, matching fails. $eq - `{: {$eq: string||number||boolean||object value}}` -* Returns the result of ` == value` or in the case of `object`, is deep equal to. +* If ` == value` or in the case of `object`, is deep equal to, matching continues; otherwise, matching fails. -$every - +$every - `{: {$every: (any item,any key,iterable) =>}}` -$excludes - +* Returns `true` if the provided function returns `truthy` for every value in the `Iterable` stored in the target `` and matching continues; otherwise, matching fails. Fails if the value of the target `` is not an `Iterable`. Unlike its JavaScript counterpart, the target `` value can be any type of `Iterable`, not just an `Array` and if the function returns a `Promise`, it will be awaited. + +$excludes - `{: {$excludes: any value}}` + +* Converse of `$in` and `$includes`. $extract - `{: {$extract: }}` Replaces the value of the `` on the target with only the portions of its children explicity referenced in ``. -$false - +$false - `{: {$false: null}}` + +* Returns false and matching fails; otherwise, matching continues. To compare a value to `false`, use `$eq` or `$eeq`. -$filter - +$filter - `{: {$filter: function (any value) => ...}}` -$forDescendant - `{: {$forDescendant: {pattern,f,depth=Infinity}}` +* Sets `` on target to an `Array` after filtering the `Iterable` value of the property using `f`. Since `f` is an in place operation, no `as` alias is available, use `$map` for this purpose. Unlike its JavaScript counterpart, general `Iterable` values are supported, not just `Array` values. If no target `` exists or it is not an `Iterable`, returns `false` and matching fails. + +$forDescendant - `{: {$forDescendant: {,function f,number depth=Infinity}}` Walks the descendant tree of the `: {$forEach: (value,key,iterable) => ...}}` + +* Sets `` on target to an `Array` after applying the supplied function to the `Iterable` value of the property. Unlike its JavaScript counterpart, general `Iterable` values are supported, not just `Array` values and if the function returns a `Promise`, it will be awaited. If no target `` exists, `false` is returned and matching fails. + +$freeze - `{: {$freeze: deep}}` + +* Sets `` on target to frozen version of itself. If `deep` is true, applies to child objects also. If value is `null` or `undefined`, sets to `Object.freeze({})`. $fullYear - `{: {$fullYear: number fullYear}}` -* Returns true if the target `` has a Date value where `getFullYear() === fullYear`. +* If the target `` has a Date value where `getFullYear() === fullYear`, matching continues; otherwise, matching fails. $gt - `{: {$gte: string||number||boolean value}}` -* Returns the result of ` > value`. +* If ` > value`, matching continues; otherwise, matching fails. $gte - `{: {$gte: string||number||boolean value}}` -* Returns the result of ` >= value`. +* If ` >= value`, matching continues; otherwise, matching fails. $hours - `{: {$hours: number hours}}` -* Returns true if the target `` has a Date value where `getHours() === hours`. +* If the target `` has a Date value where `getHours() === hours`, matching continues; otherwise, matching fails. + +$in - `{: {$in: object container}}` + +* If the target `` value is contained in the `container`, returns `true` and matching continues; otherwise, matching fails. The function is polymorphic, it will work if the `container` is an `Iterable` or supports the method `in` or `container` supports the method `includes` or `contains`. -$in - +$includes - `{: {$includes: object container}}` -$includes - +* The same as `$in`, except the order is swapped. -$instanceof - +$instanceof - `{: {$instanceof: string className||function ctor}}` + +* If a `className` is provided, it is used to look-up `ctor` in the JOQULAR contructor registry maitained by using `JOQULAR.register(ctor[,name)` and `JOQULAR.unregister(ctor[,name)`. If ` instanceof ctor` returns `true` and matching continues; otherwise, matching fails. $intersection - `{: {$intersection: Array array}}` -* Sets the `` on the target to the intersection of the array on the target with `array`. If the property does not contain an array, the result is an emtpy array since there can be no intersection. If `array` has an `as` and additional property is added to hold the intersection. +* Sets the `` on the target to the intersection of the array on the target with `array`. If the property does not contain an array, the matching fails. If `array` has an `as` and additional property, it is added to hold the intersection. To set the `as` property on an array, pass in this self evaluating function: `((array, as) => { array.as = as; return array; })()`. $intersects - `{: {$intersects: Array array}}` -* Returns true if the array on the target `` intersects with `array`. +* If the array on the target `` intersects with `array`, matching continues; otherwise, matching fails. $isAny - `{: {$isAny: types=["boolean","function","number","object","string"]}}` -* Returns the result of `types.includes()`. By default this is all types except "undefined". +* If `types.includes()` is `true`, matching continues; otherwise, matching fails. By default this is all types except "undefined". $isArray - `{: {$isArray: null}}` -* Returns the result of `Array.isArray()`. +* If `Array.isArray()` is `true`, matching continues; otherwise, matching fails. + +$isCreditCard - `{: {$isCreditCard: null}}` -$isCreditCard - `{: {$isCreditCard: string||number||boolean value}}` +* If the value of the target `` looks like a credit card number and satifies the Luhn algorithm, matching continues; otherwise, matching fails. -* Returns true if the value of the target `` satifies the Luhn algorithm. +$isEmail - `{: {$isEmail: null}}` -$isEmail - +* If the value of the target `` looks like an e-mail address, matching continues; otherwise, matching fails. $isEven - `{: {$isEven: null}}` -* Returns the result of ` % 2 === 0`. +* If ` % 2 === 0`, matching continues; otherwise, matching fails. $isFrozen - `{: {$isFrozen: null}}` -* Returns the result of `Object.isFrozen()`. +* If `Object.isFrozen()` is `true`, matching continues; otherwise, matching fails. $isIPAddress `{: {$isIPAddress: null}}` -* Returns `true` is the target `` satifies the regular expression: +* If the target `` satisfies the regular expression is `true`, matching continues; otherwise, matching fails. * (/^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/m). $isNaN - `{: {$isNan: null}}` -* Returns true if the value of the target `` `satisfies isNaN(value)`. +* If the value of the target `` satisfies `isNaN(value)` is `true`, matching continues; otherwise, matching fails. $isOdd - `{: {$isOdd: null}}` -* Returns the result of ` % 2 !== 0` +* If ` % 2 !== 0`, matching continues; otherwise, matching fails. $isSSN - `{: {$isSSN: null}}` -* Returns `true` if the target `` value satisfies the regular expression: +* If the target `` value satisfies the regular expression, matching continues; otherwise, matching fails. * /^\d{3}-?\d{2}-?\d{4}$/. $length - `{: {$length: value}}` -* Returns the result of `.length === value`. +* If `.length === value`, matching continues; otherwise, matching fails. $lt - `{: {$lt: string||number||boolean value}}` -* Returns the result of ` < value`. +* If ` < value`, matching continues; otherwise, matching fails. $lte - `{: {$lte: string||number||boolean value}}` -* Returns the result of ` <= value`. +* If ` <= value`, matching continues; otherwise, matching fails. + +$lock - `{: {$lock: null}}` + +* Modifies the `` on the target to be non-configurable and non-writable. + +$map - `{: {$map: function f||Array [function f,string as]}}` + +* Sets `` on target to an `Array` resulting from mapping the `Iterable` value of the property using the provided function `f`. If `as` is provided an additional property is added instead of setting ``. Unlike its JavaScript counterpart, general `Iterable` values are supported, not just `Array` values. If no target `` exists, `false` is returned and matching fails. The function `f` should take the form `(value,index,iterable) => ...`. -$map - +$match - `{: {$match: any value}}` -$matches - +* If the target `` value is `undefined` or `null` fails and matching stops. `$match` is polymorphic. If the target `` value is a `boolean`, `number`, or `string`, the `value` is assumed to be a `RegExp` or a string delimited with `/`s convertable into a `RegExp`; otherwise, the value of the `target` property must support the method `match` or `matches`. If the `RegExp` has matches or the method returns `true` matching continues; otherwise matching fails. + +$matches - `{: {$matches: any value||Array [any value,string as]}}` + +* If the target `` value is `undefined` or `null` fails and matching stops. `$matches` is polymorphic. If the target `` value is a `boolean`, `number`, or `string`, the `value` is assumed to be a `RegExp` or a string delimited with `/`s convertable into a `RegExp`; otherwise, the value of the `target` property must support the method `match` or `matches`. Sets `` on target to the the result of calling `matches(value)` or `match(value)`. $max - `{: {$max: string||number||boolean value}}` @@ -241,11 +281,11 @@ $maxa - `{: {$max: string||number||boolean value}}` $milliseconds - `{: {$milliseconds: number milliseconds}}` -* Returns true if the target `` has a Date value where `getMilliseconds() === milliseconds`. +* If the target `` has a Date value where `getMilliseconds() === milliseconds` matching continues; otherwise, matching fails. $min - `{: {$min: string||number||boolean value}}` -* Assumes the current value of the `` on the target is an iterable. Get's the min and either replaces the property value with the min if `as` equals `null`. Or, it adds a property with the name `as` and sets it to the min. +* Assumes the current value of the `` on the target is an `Iterable`. Get's the min and either replaces the property value with the min if `as` equals `null`. Or, it adds a property with the name `as` and sets it to the min. If the value is not an `Iterable`, matching fails. $mina - `{: {$min: string||number||boolean value}}` @@ -253,25 +293,29 @@ $mina - `{: {$min: string||number||boolean value}}` $minutes - `{: {$minutes: number minutes}}` -* Returns true if the target `` has a Date value where `getMinutes() === minutes`. +* If the target `` has a Date value where `getMinutes() === minutes` matching continues; otherwise, matching fails. $month - `{: {$month: number month}}` -* Returns true if the target `` has a Date value where `getMonth() === month`. +* If the target `` has a Date value where `getMonth() === month` matching continues; otherwise, matching fails. $ne - Alias for `$neq`. $neeq - `{: {$neeq: string||number||boolean||object value}}` -* Returns true if the value of the target `` `!== value`. +* If `!== value` matching continues; otherwise, matching fails. $neq - `{: {$neq: string||number||boolean||object value}}` -* Returns true if the value of the target `` `!= value` or in the case of an object are not deep equal. +* If ` != value` or in the case of an object are not deep equal matching continues; otherwise, matching fails. + +$nin - `{:{$nin: object value}}` -$nin +* Converse of `$in`. -$not +$not - `{:{$not: }}` + +Returns `true` and matching continues is the value of the target `:{$on:{get,set,delete,onError}}}` @@ -279,93 +323,137 @@ Adds handlers to the target `` that are invoked for `get`, `set`, and If `onError` is provided it should have the signature `(error,object,key,value[,oldvalue]) => ...`. The `onError` function can chose to swallow the error or re-throw it. Note, if the error is critical and the desrie is to abort the attempted action, the `onError` handler must be synchronus, i.e. not return a `Promise` or throw from a timeout. -$or - +$or - `{:{$or: Array [,...]||}}` + +Returns `true` and matching continues if the value of the target `` satisfies one of the JOQULAR patterns in the `Array` or a nested logical pattern, e.g. `{$or: {$and: ...}`; otherwise, matching fails. -$outside - +$outside - `{: {$outside: [number hi,number lo]}}` + +* Returns `true` and matching continues if the value of `` on the target is outside `hi` and `lo`. The value of the target `` can be a Date in addition to a `number` or a `string`. The function is polymorphic. If the method `outside` is available on the value of the target ``, it is called with `hi` and `lo` as arguments. Hence, it can be used for geometric objects, 3D space, or other custom classes. $readonly - `{: {$readonly: null}}` -Returns true if the `` exists on the target and is read-only. +Returns `true` and continue matching if the `` exists on the target and is read-only; otherwise, matching fails. $redact - `{: {$redact null||(key,value,object) => ...}}` -Deletes the `` from the target if argument is `null` or the provided function returns true. +Deletes the `` from the target if argument is `null` or the provided function returns `true`. + +$reduce - `{: {$reduce: function f||Array [function f,accum,as]}}` + +* Sets `` on target to an `Array` resulting from reducing the `Iterable` value of the property using the provided function `f`. If `accum` is not provided, the first value of the `Iterable` is used. If `as` is provided an additional property is added instead of setting ``. Unlike its JavaScript counterpart, general `Iterable` values are supported, not just `Array` values. If no target `` exists, `false` is returned and matching fails. The function `f` should take the form `(accum,value,index,iterable) => ...`. -$reduce - +$regexp - `{: {$regexp: RegExp regexp}}` -$regexp - +Alias for `$match`. $return - `{: {$return: any value}}` Sets the value on the target `` to `value`. -$sample - `{: {$sample: number 0.percentage||[pct,max=Infinity,as]}}` +$sample - `{: {$sample: number 0.pct||[0.pct,number max=Infinity,string as]}}` -Assumes the target `` is an `Iterable` and computes a random sample of `max` size or `percentage * size`. The sample is stored in the target property unless `as` is specified to add a property. The `percentage` is a decimal between zero and one. The chance of any given item being picked is independenty equal to the `percentage`. This is because for some iterables the size is not known until the entire iterable has been processed. Be careful not to try and sample an infinitely yielding `Iterator` without providing a length. +Assumes the target `` is an `Iterable` and computes a random sample of `max` size or `percentage * size`. The sample is stored in the target property unless `as` is specified to add a property. The `pct` is a decimal between zero and one. The chance of any given item being picked is independenty equal to the `pct`. This is because for some iterables the size is not known until the entire iterable has been processed. Be careful not to try and sample an infinitely yielding `Iterator` without providing a length. If the value of the target `` is not an iterable, matching fails. -$search - +$search - `{: {$search: string searchPhrase||[string searchPhrase,number stems,number trigrams=0.8,string language]}` + +* Returns `true` and matching continues if `searchPhrase` is found in the value of the target ``. The search is conducted using stemmed tokens and trigrams. The stem matching is uses `or` unless `stems` is set to a number between zero and 1 to use as a percentage stem match . If stem matching fails, trigram matching is attempted must equal or exceed 80%. To turn off trigram matching, explicitly pass `trigrams=1.0`. `$search` is polymorphic. If the target `` value supports the method `search` then it is called with `searchPhrase` as the first argument and `{language,stems,trigrams}` as the second. The only language currently supported is "en", English. $seconds - `{: {$seconds: number seconds}}` -* Returns true if the target `` has a Date value where `getSeconds() === seconds`. +* Returns `true` if the target `` has a Date value where `getSeconds() === seconds`. + +$some - `{: {$some: (any item,any key,Iterable iterable) =>}}` + +* Returns `true` if the provided function returns `truthy` for some value in the `Iterable` stored in the target `` and matching continues; otherwise, matching fails. Fails if the value of the target `` is not an `Iterable`. Unlike its JavaScript counterpart, the target `` value can be any type of `Iterable`, not just an `Array` and if the function returns a `Promise`, it will be awaited. -$some - +$sort - `{: {$sort: (any a, any b) => ... || [(any a,any b) => ...,as]}}` -$sort - +* Assumes the current value of the `` on the target is an iterable. Sorts the values using the normal JavaScript sort approach and replaces the property value with the sorted value i `as` equals `null`. Or, it adds a property with the name `as` and sets it to the average. If the value of the target `` is not an iterable, matching fails. $sum - `{: {$sum: null||string as}}` -* Assumes the current value of the `` on the target is an iterable. Average's the numbers and either replaces the property value with the average if `as` equals `null`. Or, it adds a property with the name `as` and sets it to the average. +* Assumes the current value of the `` on the target is an iterable. Averages the numbers and either replaces the property value with the average if `as` equals `null`. Or, it adds a property with the name `as` and sets it to the average. If the value of the target `` is not an iterable, matching fails. $suma - `{: {$sum: null||string as}}` * Same as `$sum`, except `true` and `false` are treated like 1 and 0 respectively and an attempt is made to parse string values as numbers. -$text - +$text - `{: {$text: string searchPhrase||[string searchPhrase,string language]}` + +Aliased to `$search`. Unlike MongoDB, case sensisitivity is not supported and the search uses stems and trigrams. $time - `{: {$time: number time}}` -* Returns true if the target `` has a Date value where `getTime() === time`. +* Returns `true` and matching continues if the target `` has a Date value where `getTime() === time`; otherwise, matching fails. + +$true - `{: {$true: null}}` -$true - +* Returns `true` and matching continues; otherwise, matching fails. To compare a value to `true`, use `$eq` or `$eeq`. $type - Alias for `$typeof`. $typeof - `{: {$typeof: string type}}` -* Returns true if the value of the target `` `== type`. +* Returns `true` if the value of the target `` `== type`. + +$UTCDate - `{: {$UTCDate: number date}}` + +* Same as $date, except for UTC value. -$UTCDate +$UTCDay -`{: {$UTCDay: number day}}` -$UTCDay +* Same as $day, except for UTC value. -$UTCFullYear +$UTCFullYear - `{: {$UTCFullYear: number fullYear}}` -$UTCHours +* Same as $fullYear, except for UTC value. -$UTCMilliseconds +$UTCHours -`{: {$UTCHours number hours}}` -$UTCMinutes +* Same as $hours, except for UTC value. -$UTCMonth +$UTCMilliseconds -`{: {$UTCMilliseconds: number milliseconds}}` -$UTCSeconds +* Same as $milliseconds, except for UTC value. -$valid +$UTCMinutes -`{: {$UTCMinutes: number minutes}}` -$value +* Same as $minutes, except for UTC value. -$where +$UTCMonth -`{: {$UTCMonth: number month}}` -$xor +* Same as $month, except for UTC value. + +$UTCSeconds -`{: {$UTCSeconds: number seconds}}` + +* Same as $seconds, except for UTC value. + +$valid - `{: {$valid: (value,key,object) => ... || }}` + +If a function is passed in and returns `true` matching continues. If a pattern is passed in, JOQULAR confirms the `` value on the target conforms with the ``. If it does not conform and the pattern contains `onError` with a function value the function is called with `(error,value,key,object)`. If no `onError` is provided or its value is not a function, an error is thrown. + +$value - `{: {$value: (value,key,object)=>...}}` + +Alias for `$compute`. + +$where - `{: {$where: (value,key,object)=>...}}` + +Alias for `$`. + +$xor - `{:{$xor: Array [,...]||}}` + +Returns `true` and matching continues if the value of the target `` satisfies at most one of the JOQULAR patterns in the `Array` or a nested logical pattern, e.g. `{$or: {$and: ...}`; otherwise, matching fails. $year - `{: {$year: number year}}` -* Returns true if the target `` has a Date value where `getYear() === year`. +* Returns `true` if the target `` has a Date value where `getYear() === year`. ## Updates +2019-02-12 v2.03b - All functions now have basic documentation. + 2019-02-11 v2.02b - Lots of documentation. Started deprecation on method name `.match` in favor of `.query`, but both will work for now through aliasing. 2019-02-10 v2.0.1b - `.match` now returns an array and can take multiple objects to match against. Added event handlers via diff --git a/index.js b/index.js index fdbe99f..5643f2b 100644 --- a/index.js +++ b/index.js @@ -35,6 +35,215 @@ return g; } + function trigrams(tokens) { + const grams = [], + str = Array.isArray(tokens) ? tokens.join("") : tokens+""; + for(let i=0;i"'\{\}\[\]\(\)\-\=\+\*\~\n\t\:\.\;\:\$\#\%\&\*\^\!\~\<\>\,\?\`\'\"]/g," ").toLowerCase().split(" "); } + const CTORS = { Object, @@ -125,6 +334,12 @@ } return a===b; }, + deepFreeze = (object) => { + if(object && typeof(object)==="object") { + Object.keys(object).forEach(key => deepFreeze(object[key])); + Object.freeze(object); + } + }, deleteFunction = (name) => { if(!name || name==="anonymous") { throw new Error("JOQULAR.function: A function name must be provided"); @@ -175,6 +390,9 @@ vtype = typeof(value); if(pattern && ptype=="object") { for(const key in pattern) { + if(key==="onError") { + continue; + } let pkey = key; if(key==="$_") { pkey = () => true; @@ -369,6 +587,12 @@ $disjoint(a,b) { return !this.$intersects(a,b); }, + $disjunction(a,b) { + if(Array.isArray(a) && Array.isArray(b)) { + this[b.as||key] = disjunction(a,b); + return true; + } + }, $echoes(a,b) { return soundex(a)===soundex(b); }, @@ -396,8 +620,9 @@ } key++; } + return true; } - return true; + return false; }, async $extract(target,pattern,key) { const extracted = {}; @@ -445,16 +670,30 @@ } return true; }, - $freeze(value,property,key) { - const type = typeof(value); - if(value && type==="object") { - Object.freeze(value); + async $forEach(iterable,f,key) { + if(Symbol.iterator in Object(iterable)) { + let key = 0; + for(let value of iterable) { + if(Array.isArray(iterable)) { + results.push(await f(value,key,iterable)); + } else { + results.push(await f(value[1],value[0],iterable)); + } + key++; + } + return true; } - if(property) { - try { - Object.defineProperty(this,key,{enumerable:true,value}); - } catch(e) { - ; + return false; + }, + $freeze(value,deep,key) { + if(value==null) { + value = {}; + } + if(value && typeof(value)==="object") { + if(deep) { + deepFreeze(value); + } else { + Object.freeze(value); } } return true; @@ -472,24 +711,34 @@ if(value && typeof(value.in)==="function") { return value.in(includer); } + if(value && typeof(value.has)==="function") { + return value.has(includer); + } if(includer && typeof(includer.includes)==="function") { return includer.includes(value); } - }, - $includes(includer,value) { - if(includer) { - if(typeof(includer.includes)==="function") { - return includer.includes(value); - } - if(typeof(includer.excludes)==="function") { - return !includer.excludes(value); + if(includer && typeof(includer.contains)==="function") { + return includer.contains(value); + } + if(Symbol.iterator in Object(includer)) { + for(let item of includer) { + if(item[1]===value) return true; } } }, + $includes(includer,value) { + return FUNCTIONS.$in(value,includer); + }, $instanceof(a,b) { b = typeof(b)==="string" ? CTORS[b] : b; return a && typeof(a)==="object" && b && typeof(b)==="function" && a instanceof b; }, + $intersects(a,b,key) { + if(Array.isArray(a) && Array.isArray(b)) { + this[b.as||key] = intersection(a,b); + return true; + } + }, $intersects(a,b) { return Array.isArray(a) && Array.isArray(b) && intersection(a,b).length>0; }, @@ -531,6 +780,14 @@ $length(value,length) { return a && a.length===length; }, + $lock(value,_,key) { + try { + Object.defineProperty(this,key,{enumerable:true,value}); + } catch(e) { + ; + } + return true; + }, $lt(a,b) { return a < b; }, @@ -538,9 +795,9 @@ return a <= b; }, async $map(iterable,spec,key) { - const results = []; - let [f,as] = Array.isArray(spec) ? spec : [spec]; if(Symbol.iterator in Object(iterable)) { + const results = []; + let [f,as] = Array.isArray(spec) ? spec : [spec]; let key = 0; for(let value of iterable) { if(Array.isArray(iterable)) { @@ -550,24 +807,58 @@ } key++; } + this[as||key] = results; + return true; } - this[as||key] = results; - return true; + return false; }, - $matches(value,regexp,key) { - if(value) { - const query = value.matches||value.query; - if(typeof(query)==="function") { - let as; - if(Array.isArray(regexp)) { - as = regexp[2]; - try { - regexp = new RegExp(...regexp.slice(0,2)); - } catch(e) { - ; + $match(value,match,key) { + if(value!=null) { + const type = typeof(value); + if(["boolean","number","string"].includes(type)) { + value += ""; + if(typeof(match)==="string" && match[0]==="/") { + const parts = match.split("/"); + if(parts.length===3) { + try { + match = new RegExp(parts[1],parts[2]); + } catch(e) { + ; + } } } - const matches = query(regexp); + } + if(value.matches) { + const matches = value.matches(match); + if(matches && matches.length>0) { + return true; + } + } + } + }, + $matches(value,match,key) { + if(value!=null) { + const type = typeof(value); + let as; + if(Array.isArray(match)) { + as = match[1]; + match = match[0]; + } + if(["boolean","number","string"].includes(type)) { + value += ""; + if(typeof(match)==="string" && match[0]==="/") { + const parts = match.split("/"); + if(parts.length===3) { + try { + match = new RegExp(parts[1],parts[2]); + } catch(e) { + ; + } + } + } + } + if(value.matches||value.match) { + const matches = (value.matches||value.match)(match); if(matches) { this[as||key] = matches; return true; @@ -655,12 +946,7 @@ return a !== b; }, $nin(value,includer) { - if(value && typeof(value.in)==="function") { - return !value.in(includer); - } - if(includer && typeof(includer.includes)==="function") { - return !includer.includes(value); - } + return !FUNCTIONS.$in(includer,value); }, async $not(a,tests,key) { const resolve = (a,pname,value) => FUNCTIONS[pname] ? FUNCTIONS[pname].call(this,a,value,key) : false, @@ -778,7 +1064,7 @@ } } }, - $outside(value,[lo,hi]) { + $outside(value,[lo,hi],key) { if(value) { if(typeof(value.outside)==="function") { return value.outside(lo,hi); @@ -807,20 +1093,29 @@ return true; }, async $reduce(iterable,spec,key) { - let [f,accum,as] = Array.isArray(spec) ? spec : [spec]; if(Symbol.iterator in Object(iterable)) { - let key = 0; + let [f,accum,as] = Array.isArray(spec) ? spec : [spec], + key = 0; for(let value of iterable) { if(Array.isArray(iterable)) { - accum = await f(accum,value,key,iterable); + if(accum===undefined && key===0) { + accum = value; + } else { + accum = await f(accum,value,key,iterable); + } } else { - accum = await f(accum,value[1],value[0],iterable); + if(accum===undefined && key===0) { + accum = value[0]; + } else { + accum = await f(accum,value[1],value[0],iterable); + } } key++; } + this[as||key] = accum; + return true; } - this[as||key] = accum; - return true; + return false; }, $return(_,value,key) { if(value===undefined) { @@ -831,12 +1126,12 @@ return true; }, $sample(iterable,spec,key) { - let [pct,max=Infinity,as] = Array.isArray(spec) ? spec : [spec]; - const sample = [], - indexes = []; - let i = 0; // get count random items from an iterable if(Symbol.iterator in Object(iterable)) { + let [pct,max=Infinity,as] = Array.isArray(spec) ? spec : [spec]; + const sample = [], + indexes = []; + let i = 0; for(const item of iterable) { i++; const rand = Math.random(); @@ -849,26 +1144,59 @@ if(sample.length===max) break; } } + const length = Math.min(max,Math.round(i * pct)); + this[as||key] = sample.slice(0,length); + return true; } - const length = Math.min(max,Math.round(i * pct)); - this[as||key] = sample.slice(0,length); - return true; + return false; }, async $search(text,phrase) { if(text) { - if(typeof(text.search)==="function") { - return text.search(phrase); + let language, + stems, + tris; + if(Array.isArray(phrase)) { + stems = phrase[1], + tris = phrase[2], + language = phrase[3], + phrase = phrase[0] } - const tokens = tokenize(phrase), - stems = stems(tokens); - if(stems.some(stem => text.includes(stem))) { - return true; + if(typeof(tris)!=="number" || tris===0) { + tris = 0.8; + } + if(typeof(language)!=="string") { + language = "en"; } - const tris = trigrams(phrase.replace(/\s/g,"")), - count = tris.reduce((accum,tri) => { return text.includes(tri) ? accum++ : accum },0); - if((count/tris.length)>.8) { + const stops = STOPWORDS[language] || STOPWORDS.en; + + if(typeof(text)!=="string") { + if(typeof(text.search)==="function") { + return text.search(phrase,{stems,trigrams,language}); + } + return false; + } + let tokens = tokenize(phrase), + stemmed = tokens.map(token => stemmer(token)).filter(stem => !stops.includes(stem)); + if(!stems && stemmed.some(stem => text.includes(stem))) { return true; } + if(!stems && !tris) { + return false; + } + tokens = tokenize(text); + if(stems) { + const count = tokens.reduce((accum,token) => stemmed.some(stem => token.includes(stem)) ? accum += 1 : accum,0); + if(count/tokens.length>=stems) { + return true; + } + } + if(tris) { + const grams = trigrams(tokens.join("")), + count = grams.reduce((accum,tri) => text.includes(tri) ? accum += 1 : accum,0); + if((count/grams.length)>tris) { + return true; + } + } } }, async $some(iterable,f) { @@ -890,15 +1218,18 @@ return false; }, $sort(sortable,spec,key) { - let [f,as] = Array.isArray(spec) ? spec : [spec], - result; - if(sortable && typeof(sortable.sort)==="function") { - result = sortable.sort((a,b) => f(a,b)); - } else { - result = []; + if(Array.isArray(sortable)) { + sortable = sortable.slice(); + let [f,as] = Array.isArray(spec) ? spec : [spec], + result; + if(sortable && typeof(sortable.sort)==="function") { + result = sortable.sort((a,b) => f(a,b)); + } else { + result = []; + } + this[as||key] = result + return true; } - this[as||key] = result - return true; }, $sum(iterable,as,key) { if(Symbol.iterator in Object(iterable)) { @@ -953,7 +1284,7 @@ const valid = validator(this,validations[validationKey]); if(!valid) { const error = new TypeError(`failed validation ${validationKey} for ${this}`); - if(validations.onError) { + if(typeof(validations.onError)==="function") { validations.onError(error,value,key,this); } else { throw(error); diff --git a/package.json b/package.json index bad47c3..229603a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "joqular", - "version": "2.0.2b", + "version": "2.0.3b", "description": "JOQULAR (JavaScript Object Query Language Representation) for JSON data matching, transformation, validation and extraction", "main": "index.js", "scripts": { diff --git a/test/index.js b/test/index.js index 8f65ead..a7e6545 100644 --- a/test/index.js +++ b/test/index.js @@ -297,7 +297,7 @@ describe("Test",function() { return JOQULAR.query({data:{$sample:[.5,5]}},testobject) .then(([object]) => { testvalidator(object); - chai.expect(object.data.length).equal(5); + chai.expect(object.data.length==4||object.data.length==5).equal(true); }) }); it("$echoes",function() { @@ -406,4 +406,22 @@ describe("Test",function() { chai.expect(event[2]).equal(undefined); }) }); + it("$search",function() { + return JOQULAR.query({email:{$search:"someone"}},testobject) + .then(([object]) => { + testvalidator(object); + }) + }); + it("$search pct",function() { + return JOQULAR.query({email:{$search:["someone",.5]}},testobject) + .then(([object]) => { + testvalidator(object); + }) + }); + it("$search trigram",function() { + return JOQULAR.query({email:{$search:["someone",1.0]}},testobject) + .then(([object]) => { + testvalidator(object); + }) + }) }); \ No newline at end of file