-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathhelpers.js
More file actions
195 lines (174 loc) · 5.8 KB
/
helpers.js
File metadata and controls
195 lines (174 loc) · 5.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
/* eslint-disable no-var */
/* eslint-disable camelcase */
const operations = require('./operations')
function truthy (value) {
if (Array.isArray(value) && value.length === 0) {
return false
}
return !!value
}
function is_logic (logic) {
return (
typeof logic === 'object' && // An object
logic !== null && // but not null
!Array.isArray(logic) && // and not an array
Object.keys(logic).length === 1 // with exactly one key
)
}
function get_operator (logic) {
return Object.keys(logic)[0]
}
function arrayUnique (array) {
var a = []
for (var i = 0, l = array.length; i < l; i++) {
if (a.indexOf(array[i]) === -1) {
a.push(array[i])
}
}
return a
}
function apply (logic, _data) {
// Does this array contain logic? Only one way to find out.
if (Array.isArray(logic)) {
return logic.map(function (l) {
return apply(l, _data)
})
}
// You've recursed to a primitive, stop!
if (!is_logic(logic)) {
return logic
}
const data = Object.assign({}, _data) || {}
var op = get_operator(logic)
var values = logic[op]
var i
var current
var scopedLogic, scopedData, filtered, initial
// easy syntax for unary operators, like {"var" : "x"} instead of strict {"var" : ["x"]}
if (!Array.isArray(values)) {
values = [values]
}
// 'if', 'and', and 'or' violate the normal rule of depth-first calculating consequents, let each manage recursion as needed.
if (op === 'if' || op === '?:') {
/* 'if' should be called with a odd number of parameters, 3 or greater
This works on the pattern:
if( 0 ){ 1 }else{ 2 };
if( 0 ){ 1 }else if( 2 ){ 3 }else{ 4 };
if( 0 ){ 1 }else if( 2 ){ 3 }else if( 4 ){ 5 }else{ 6 };
The implementation is:
For pairs of values (0,1 then 2,3 then 4,5 etc)
If the first evaluates truthy, evaluate and return the second
If the first evaluates falsy, jump to the next pair (e.g, 0,1 to 2,3)
given one parameter, evaluate and return it. (it's an Else and all the If/ElseIf were false)
given 0 parameters, return NULL (not great practice, but there was no Else)
*/
for (i = 0; i < values.length - 1; i += 2) {
if (truthy(apply(values[i], data))) {
return apply(values[i + 1], data)
}
}
if (values.length === i + 1) return apply(values[i], data)
return null
} else if (op === 'and') { // Return first falsy, or last
for (i = 0; i < values.length; i += 1) {
current = apply(values[i], data)
if (!truthy(current)) {
return current
}
}
return current // Last
} else if (op === 'or') { // Return first truthy, or last
for (i = 0; i < values.length; i += 1) {
current = apply(values[i], data)
if (truthy(current)) {
return current
}
}
return current // Last
} else if (op === 'filter') {
scopedData = apply(values[0], data)
scopedLogic = values[1]
if (!Array.isArray(scopedData)) {
return []
}
// Return only the elements from the array in the first argument,
// that return truthy when passed to the logic in the second argument.
// For parity with JavaScript, reindex the returned array
return scopedData.filter(function (datum) {
return truthy(apply(scopedLogic, datum))
})
} else if (op === 'map') {
scopedData = apply(values[0], data)
scopedLogic = values[1]
if (!Array.isArray(scopedData)) {
return []
}
return scopedData.map(function (datum) {
return apply(scopedLogic, datum)
})
} else if (op === 'reduce') {
scopedData = apply(values[0], data)
scopedLogic = values[1]
initial = typeof values[2] !== 'undefined' ? values[2] : null
if (!Array.isArray(scopedData)) {
return initial
}
return scopedData.reduce(
function (accumulator, current) {
return apply(
scopedLogic,
{ current: current, accumulator: accumulator }
)
},
initial
)
} else if (op === 'all') {
scopedData = apply(values[0], data)
scopedLogic = values[1]
// All of an empty set is false. Note, some and none have correct fallback after the for loop
if (!scopedData.length) {
return false
}
for (i = 0; i < scopedData.length; i += 1) {
if (!truthy(apply(scopedLogic, scopedData[i]))) {
return false // First falsy, short circuit
}
}
return true // All were truthy
} else if (op === 'none') {
filtered = apply({ filter: values }, data)
return filtered.length === 0
} else if (op === 'some') {
filtered = apply({ filter: values }, data)
return filtered.length > 0
}
// Everyone else gets immediate depth-first recursion
values = values.map(function (val) {
return apply(val, data)
})
// The operation is called with "data" bound to its "this" and "values" passed as arguments.
// Structured commands like % or > can name formal arguments while flexible commands (like missing or merge) can operate on the pseudo-array arguments
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/arguments
if (typeof operations[op] === 'function') {
return operations[op].apply(data, values)
} else if (op.indexOf('.') > 0) { // Contains a dot, and not in the 0th position
var sub_ops = String(op).split('.')
var operation = operations
for (i = 0; i < sub_ops.length; i++) {
// Descending into operations
operation = operation[sub_ops[i]]
if (operation === undefined) {
throw new Error('Unrecognized operation ' + op +
' (failed at ' + sub_ops.slice(0, i + 1).join('.') + ')')
}
}
return operation.apply(data, values)
}
const err = new Error('Unrecognized operation ' + op)
err.data = data
err.logic = logic
throw err
}
module.exports.apply = apply
module.exports.truthy = truthy
module.exports.arrayUnique = arrayUnique