Skip to content

Commit

Permalink
Merge pull request #13 from muze-nl/v0.9.3
Browse files Browse the repository at this point in the history
V0.9.3
  • Loading branch information
poef authored Sep 18, 2024
2 parents c991f66 + f0a3b6e commit fffee2d
Show file tree
Hide file tree
Showing 6 changed files with 232 additions and 33 deletions.
120 changes: 119 additions & 1 deletion docs/manual.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
- [Nesting select()](#nesting-select)
- [Performance](#performance)
- [More uses of `_`](#_)
- [Extending JAQT](#extending)

<a name="introduction"></a>
## Introduction
Expand Down Expand Up @@ -1229,4 +1230,121 @@ Will only return the first entry:
{ hair_color: 'blond', skin_color: 'fair', eye_color: 'blue' }
```

Note that you must use the `[]` syntax to access numeric properties, only properties starting with a letter (or `$` or `_`) can be used with the `.` notation.
Note that you must use the `[]` syntax to access numeric properties, only properties starting with a letter (or `$` or `_`) can be used with the `.` notation.

<a name="extending"></a>
## Extending JAQT

JAQT comes with a lot of helper functions out of the box, but you may want to add your own. Here is how you can do that for the different parts of JAQT - select, where, orderBy and reduce:

### Select functions

JAQT provides the functions `one`,`many` and `first` to be used inside the pattern for `select`. For example: `many` is defined as:

```javascript
export function many(pointerFn)
{
return (data, key) => {
let result = pointerFn(data, key)
if (result == null) {
result = []
} else if (!Array.isArray(result)) {
result = [result]
}
return result
}
}
```
Any helper function for the `select` method, must return a function with 2 parameter:

- data: the object to select properties from
- key: the left hand name/property in the select pattern

The helper function itself can have any parameters, but should probably start with the pointer function, usually `_` or `_.some.property`.

### Where functions

JAQT provides the following matching functions: `not`,`anyOf` and `allOf`. Here is how you could create your own `between` matching function:

```javascript
export function between(min, max)
{
return (data) => {
return (data <= max && result >= min)
}
}
```

Now you can use this as:

```javascript
from(data.people)
.where({
metrics: {
height: between(170, 190))
}
})
.select({
name: _
})
```

If you want to build a function like `anyOf`, which accepts multiple match criteria, you'll need to use the provided `getMatchFn` function, like this:

```javascript
import { getMatchFn } from '@muze-nl/jaqt'

function anyOf(...patterns)
{
let matchFns = patterns.map(getMatchFn)
return data => matchFns.some(fn => fn(data))
}
```

Now you can use any valid expression for the `where` function as parameter for your function.

### Reduce functions

JAQT provides `sum`, `avg`, `count`,`max`,`min` and `distinct` functions out of the box. Here is the definition of `avg`:

```javascript
function avg(fetchFn)
{
return (accu, ob, index, list) => {
accu += parseFloat(fetchFn(ob)) || 0
if (index == (list.length-1)) {
return accu / list.length
}
return +accu
}
}
```

Usually if you want to calculate the average of an array of numbers, you just add them up and divide the remainder by the length of the array. In this case, reduce gives you the index of the current entry `ob` and the list itself, so we can check if the current entry is the last of the array. And only then divide the sum. This saves a lot of divisions, but you do risk the danger of overflowing the `accu` number, if you work with a lot of large numbers.

An alternative is to keep track of the average up to this point:

```javascript
function avg(fetchFn)
{
return (accu, ob, index) => {
return +accu + ((parseFloat(fetchFn(ob)) || 0) - accu) / (index+1)
}
}
```

Now you have less risk of overflowing `accu`, but this will take longer to calculate.

You may have spotted these lines:
```javascript
return +accu
```

This is because if you call reduce, like this:
```javascript
from(data.people)
.reduce(avg(_.metrics.height))
```

JAQT will automatically start the `accu` accumulator with an initial value of `[]`. By returning `+accu` this is automatically converted to `0`.

11 changes: 11 additions & 0 deletions docs/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
- [avg()](#avg)
- [min()](#min)
- [max()](#max)
- [distinct](#distinct)
- [nested queries](#nested)

<a name="from"></a>
Expand Down Expand Up @@ -475,6 +476,16 @@ from(data.cars)
.reduce(max(_.price))
```

<a name="distinct"></a>
### distinct

Returns an array of distinct values.

```javascript
from(data.cars)
.reduce(distinct(_.category))
```

<a name="custom-reduce"></a>
### Custom reduce functions

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@muze-nl/jaqt",
"version": "0.9.2",
"version": "0.9.3",
"description": "Javascript Queries and Transformations, allows GraphQL-like functionality on Javascript arrays of objects.",
"type": "module",
"author": "Auke van Slooten <[email protected]>",
Expand Down
69 changes: 40 additions & 29 deletions src/jaqt.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@
* @param {mixed} data The data to check
* @return {Boolean} True if data is a plain object
*/
function isPlainObject(data)
function isPlainObject(data)
{
return data?.constructor === Object
|| data?.constructor === undefined // object with null prototype: Object.create(null)
}

export function one(selectFn, whichOne='last') {
export function one(selectFn, whichOne='last')
{
return (data, key, context) => {
let result = selectFn(data, key, context)
if (Array.isArray(result)) {
Expand All @@ -26,7 +27,8 @@ export function one(selectFn, whichOne='last') {
}
}

export function many(selectFn) {
export function many(selectFn)
{
return (data, key, context) => {
let result = selectFn(data, key, context)
if (result == null) {
Expand All @@ -38,7 +40,8 @@ export function many(selectFn) {
}
}

export function first(...args) {
export function first(...args)
{
return (data, key, context) => {
let result = null
for (let arg of args) {
Expand All @@ -55,14 +58,17 @@ export function first(...args) {
}
}



/**
* implements a minimal graphql-alike selection syntax, using plain javascript
* use with from(...arr).select
*
* @param {object|function} filter Which keys with which values you want
* @return Function a function that selects values from objects as defined by filter
*/
function getSelectFn(filter) {
function getSelectFn(filter)
{
let fns = []
if (filter instanceof Function) {
fns.push(filter)
Expand Down Expand Up @@ -180,7 +186,8 @@ export const desc = Symbol('desc')
* @param {mixed} pattern The comparison pattern
* @return Function The function to use with toSorted()
*/
export function getSortFn(pattern) {
export function getSortFn(pattern)
{
let comparisons = Object.entries(pattern)
let fns = []
for (let [key,compare] of comparisons) {
Expand Down Expand Up @@ -217,7 +224,8 @@ export function getSortFn(pattern) {
* @param {object|function} filter Which keys with which values you want
* @return Function a function that reduces values
*/
export function getAggregateFn(filter) {
export function getAggregateFn(filter)
{
let fns = []
if (filter instanceof Function) {
fns.push(filter)
Expand All @@ -231,14 +239,14 @@ export function getAggregateFn(filter) {
return a
})
} else if (filterValue instanceof Function) {
fns.push( (a, o) => {
fns.push( (a, o, i, l) => {
if (!isPlainObject(a)) {
a = {}
}
if (o.reduce) {
a[filterKey] = o.reduce(filterValue, a[filterKey] || [])
} else {
a[filterKey] = filterValue(a[filterKey] || [], o)
a[filterKey] = filterValue(a[filterKey] || [], o, i, l)
}
return a
})
Expand All @@ -255,10 +263,10 @@ export function getAggregateFn(filter) {
if (fns.length==1) {
return fns[0]
}
return (a, o) => {
return (a, o, i, l) => {
let result = {}
for (let fn of fns) {
Object.assign(result, fn(a,o))
Object.assign(result, fn(a, o, i, l))
}
return result
}
Expand All @@ -271,7 +279,8 @@ export function getAggregateFn(filter) {
* array is a group
*
*/
function getMatchingGroups(data, pointerFn) {
function getMatchingGroups(data, pointerFn)
{
let result = {}
for (let entity of data) {
let groups = pointerFn(entity)
Expand All @@ -298,7 +307,8 @@ function getMatchingGroups(data, pointerFn) {
* @param (object) data The data to parse and get the group from
* @param (array) properties The properties to group by, in order, should be pointer functions
*/
function groupBy(data, pointerFunctions) {
function groupBy(data, pointerFunctions)
{
let pointerFn = pointerFunctions.shift()
let groups = getMatchingGroups(data, pointerFn)
if (pointerFunctions.length) {
Expand All @@ -315,7 +325,8 @@ function groupBy(data, pointerFunctions) {
* @param fetchFn the function that fetches the correct value, e.g. _.price
* @return Function function (value, accumulator) => accumulator + value
*/
export function sum(fetchFn) {
export function sum(fetchFn)
{
return (a,o) => {
if (Array.isArray(a)) {
a = 0
Expand All @@ -331,16 +342,10 @@ export function sum(fetchFn) {
* @param fetchFn the function that fetches the correct value, e.g. _.price
* @return Function function (value, accumulator) => average(accumulator + value)
*/
export function avg(fetchFn) {
return (a,o) => {
if (Array.isArray(a)) {
a = new Number(0)
a.count = 0
}
let count = a.count+1
a = new Number(a + ((parseFloat(fetchFn(o)) || 0) - a) / count)
a.count = count
return a
export function avg(fetchFn)
{
return (a,o,count) => {
return +a + ((parseFloat(fetchFn(o)) || 0) - a) / (count+1)
}
}

Expand All @@ -350,7 +355,8 @@ export function avg(fetchFn) {
* @param fetchFn the function that fetches the correct value, e.g. _.name
* @return Function
*/
export function distinct(fetchFn) {
export function distinct(fetchFn)
{
return (a, o) => {
let v = fetchFn(o)
if (!a.includes[v]) {
Expand All @@ -366,7 +372,8 @@ export function distinct(fetchFn) {
* @param fetchFn the function that fetches the correct value, e.g. _.price
* @return Function function (value, accumulator) => accumulator + 1
*/
export function count() {
export function count()
{
return (a, o) => {
if (Array.isArray(a)) {
a = 0
Expand All @@ -381,7 +388,8 @@ export function count() {
* @param fetchFn the function that fetches the correct value, e.g. _.price
* @return Function function (value, accumulator) => Math.max(accumulator, value)
*/
export function max(fetchFn) {
export function max(fetchFn)
{
return (a,o) => {
if (Array.isArray(a)) {
a = Number.NEGATIVE_INFINITY
Expand All @@ -400,7 +408,8 @@ export function max(fetchFn) {
* @param fetchFn the function that fetches the correct value, e.g. _.price
* @return Function function (value, accumulator) => Math.min(accumulator, value)
*/
export function min(fetchFn) {
export function min(fetchFn)
{
return (a,o) => {
if (Array.isArray(a)) {
a = Number.POSITIVE_INFINITY
Expand All @@ -413,6 +422,7 @@ export function min(fetchFn) {
}
}


/**
* Not inverts the result from the matches function.
* It returns a function expecting a data parameter and inverts the result
Expand Down Expand Up @@ -654,7 +664,8 @@ export function from(data)
* @param {array} path The list of properties to access in order
* @return {function} The accessor function that returns the data matching the path
*/
function getPointerFn(path) {
function getPointerFn(path)
{
/**
* The json pointer function
* @param {mixed} data Any data
Expand Down
Loading

0 comments on commit fffee2d

Please sign in to comment.