Skip to content

Commit ae59162

Browse files
committed
feat: added Head, Tail, Numerator, Denominator, NumeratorDenominator, GCD and LCM
1 parent 593c9ce commit ae59162

19 files changed

+795
-283
lines changed

.prettierignore

+1
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@
22
/submodules/
33
/build/
44
/dist/
5+
**/docs/

CHANGELOG.md

+21-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,25 @@
1-
## 0.18.1
1+
## [Unreleased]
22

3-
**Release Date:** 2023-10-16
3+
### Improvements
4+
5+
- The functions `Sum`, `Product`, `Min`, `Max`, and the statistics functions
6+
(`Mean`, `Median`, `Variance`, etc...) now handle arguments that can be
7+
collections:
8+
9+
- `["Range"]`, `["Interval"]`, `["Linspace"]` expressions
10+
- `["List"]` or `["Set"]` expressions
11+
- `["Tuple"]`, `["Pair"]`, `["Pair"]`, `["Triple"]` expressions
12+
- `["Sequence"]` expressions
13+
14+
- Added `GCD` and `LCM` functions
15+
- Added `Numerator`, `Denominator`, `NumeratorDenominator` functions. These
16+
functions can be used on non-canonical expressions.
17+
- Added `Head` and `Tail` functions which can be used on non-canonical
18+
expressions.
19+
20+
## 0.18.1
21+
22+
**Release Date:** 2023-10-16
423

524
### Bug Fixes
625

+210
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
import { asFloat } from './numerics/numeric';
2+
import { BoxedExpression } from './public';
3+
4+
/**
5+
*
6+
* Iterate over all the expressions in an expression tree.
7+
*
8+
* Some expressions are not iterated because they are evaluated
9+
* to an "elementary" collection, for example "Fill".
10+
*
11+
* Some expressions are infinite and not iterable, for example
12+
* "Repeat", "Cycle", ...
13+
*
14+
* @param col
15+
*
16+
* @returns
17+
*/
18+
export function* iterable(
19+
col: BoxedExpression,
20+
exclude?: string[]
21+
): Generator<BoxedExpression> {
22+
const ce = col.engine;
23+
const h = col.head;
24+
25+
if (typeof h === 'string' && exclude?.includes(h)) {
26+
yield col;
27+
return;
28+
}
29+
30+
const iter = iterator(col);
31+
if (iter) {
32+
let i = 0;
33+
while (true) {
34+
const { done, value } = iter.next();
35+
if (done) return;
36+
if (i++ > ce.iterationLimit) {
37+
yield ce.error('iteration-limit-exceeded');
38+
return;
39+
}
40+
yield value;
41+
}
42+
}
43+
44+
// if (h === 'Range') {
45+
// let lower = asFloat(col.op1);
46+
// if (lower === null) return;
47+
// let upper = asFloat(col.op2);
48+
// if (upper === null) {
49+
// upper = lower;
50+
// lower = 1;
51+
// }
52+
53+
// if (!isFinite(lower) || !isFinite(upper)) return;
54+
55+
// if (lower > upper) {
56+
// const step = asFloat(col.op3 ?? -1) ?? -1;
57+
// if (step >= 0) return;
58+
// for (let i = lower; i <= upper; i += step) yield ce.number(i);
59+
// return;
60+
// }
61+
62+
// const step = asFloat(col.op3 ?? 1) ?? 1;
63+
// if (step <= 0) return;
64+
// for (let i = lower; i <= upper; i += step) yield ce.number(i);
65+
// return;
66+
// }
67+
68+
// if (h === 'Linspace') {
69+
// let start = asFloat(col.op1);
70+
// if (start === null) return;
71+
// let stop = asFloat(col.op2);
72+
// if (stop === null) {
73+
// stop = start;
74+
// start = 0;
75+
// }
76+
// const num = asFloat(col.op3) ?? 50;
77+
// if (!Number.isInteger(num)) return;
78+
// if (num <= 0) return;
79+
80+
// if (!isFinite(stop) || !isFinite(start)) return;
81+
82+
// const step = (stop - start) / (num - 1);
83+
84+
// for (let i = start; i <= stop; i += step) yield ce.number(i);
85+
// return;
86+
// }
87+
88+
// // Sequence are automatically flattended
89+
// if (h === 'Sequence') {
90+
// for (const x of col.ops!) {
91+
// if (x.head === 'Sequence') yield* each(x.ops!);
92+
// else yield x;
93+
// }
94+
// return;
95+
// }
96+
97+
// if (
98+
// typeof h === 'string' &&
99+
// /^(List|Set|Tuple|Single|Pair|Triple)$/.test(h)
100+
// ) {
101+
// for (const x of col.ops!) yield x;
102+
// return;
103+
// }
104+
105+
yield col;
106+
}
107+
108+
/**
109+
* Iterate over all the expressions in an expression tree with
110+
* the following form:
111+
* - `["Range"]`, `["Interval"]`, `["Linspace"]` expressions
112+
* - `["List"]` and `["Set"]` expressions
113+
* - `["Tuple"]`, `["Pair"]`, `["Pair"]`, `["Triple"]` expressions
114+
* - `["Sequence"]` expressions
115+
*
116+
* @param exclude a list of expression heads to exclude from the
117+
* recursive iteration. They are instead retured as is.
118+
*
119+
*
120+
*/
121+
export function* each(
122+
ops: BoxedExpression[],
123+
exclude?: string[]
124+
): Generator<BoxedExpression> {
125+
if (ops.length === 0) return;
126+
127+
for (const op of ops) for (const val of iterable(op, exclude)) yield val;
128+
}
129+
130+
/**
131+
* From an expression, create an iterator that can be used
132+
* to enumerate values.
133+
*
134+
* `expr` can be a collection, a function, an expression, a string.
135+
*
136+
* - ["Range", 5]
137+
* - ["List", 1, 2, 3]
138+
* - "'hello world'"
139+
*
140+
*
141+
*/
142+
export function iterator(
143+
expr: BoxedExpression
144+
): Iterator<BoxedExpression> | undefined {
145+
// Is it a function expresson with a definition that includes an iterator?
146+
// e.g. ["Range", 5]
147+
// Note that if there is an at() handler, there is always
148+
// at least a default iterator
149+
const def = expr.functionDefinition;
150+
if (def?.iterator) return def.iterator(expr);
151+
152+
//
153+
// String iterator
154+
//
155+
const s = expr.string;
156+
if (s !== null) {
157+
if (s.length === 0)
158+
return { next: () => ({ done: true, value: undefined }) };
159+
let i = 0;
160+
return {
161+
next: () => ({
162+
value: expr.engine.string(s.charAt(i++)),
163+
done: i > s.length,
164+
}),
165+
};
166+
}
167+
168+
return undefined;
169+
}
170+
171+
/**
172+
* indexable(expr) return a JS function with one argument.
173+
*
174+
* Evaluate expr.
175+
* If expr is indexable function (def with at handler), return handler.
176+
* Otherwise, call makeLambda, then return function that set scope
177+
* with one arg, then evaluate result of makeLambda.
178+
*/
179+
180+
// export function indexable(
181+
// expr: BoxedExpression
182+
// ): ((index: number) => BoxedExpression | undefined) | undefined {
183+
// expr = expr.evaluate();
184+
185+
// // If the function expression is indexable (it has an at() handler)
186+
// // return the at() handler, bound to this expression.
187+
// if (expr.functionDefinition?.at) {
188+
// const at = expr.functionDefinition.at;
189+
// return (index) => at(expr, index);
190+
// }
191+
192+
// //
193+
// // String at
194+
// //
195+
// const s = expr.string;
196+
// if (s !== null) {
197+
// return (index) => {
198+
// const c = s.charAt(index);
199+
// if (c === undefined) return expr.engine.Nothing;
200+
// return expr.engine.string(c);
201+
// };
202+
// }
203+
204+
// // Expressions that don't have an at() handler, have the
205+
// // argument applied to them.
206+
// const lambda = makeLambda(expr);
207+
// if (lambda) return (index) => lambda([expr.engine.number(index)]);
208+
209+
// return undefined;
210+
// }

src/compute-engine/compile.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,9 @@ const NATIVE_JS_FUNCTIONS: CompiledFunctions = {
6565
Exp: 'Math.exp',
6666
Floor: 'Math.floor',
6767
Gamma: '_SYS.gamma',
68-
Gcd: '_SYS.gcd',
68+
GCD: '_SYS.gcd',
6969
// Math.hypot
70-
Lcm: '_SYS.lcm',
70+
LCM: '_SYS.lcm',
7171
Limit: (args, compile) =>
7272
`_SYS.limit(${compile(args[0])}, ${compile(args[1])})`,
7373
Ln: 'Math.log',

src/compute-engine/function-utils.ts

-81
Original file line numberDiff line numberDiff line change
@@ -43,87 +43,6 @@ import { checkArity } from './boxed-expression/validate';
4343
*
4444
*/
4545

46-
/**
47-
* From an expression, create an iterator that can be used
48-
* to enumerate values.
49-
*
50-
* `expr` can be a collection, a function, an expression, a string.
51-
*
52-
* - ["Range", 5]
53-
* - ["List", 1, 2, 3]
54-
* - "'hello world'"
55-
*
56-
*/
57-
export function iterable(
58-
expr: BoxedExpression
59-
): Iterator<BoxedExpression> | undefined {
60-
// Is it a function expresson with a definition that includes an iterator?
61-
// e.g. ["Range", 5]
62-
// Note that if there is an at() handler, there is always
63-
// at least a default iterator
64-
const def = expr.functionDefinition;
65-
if (def?.iterator) return def.iterator(expr);
66-
67-
//
68-
// String iterator
69-
//
70-
const s = expr.string;
71-
if (s !== null) {
72-
if (s.length === 0)
73-
return { next: () => ({ done: true, value: undefined }) };
74-
let i = 0;
75-
return {
76-
next: () => ({
77-
value: expr.engine.string(s.charAt(i++)),
78-
done: i > s.length,
79-
}),
80-
};
81-
}
82-
83-
return undefined;
84-
}
85-
86-
/**
87-
* indexable(expr) return a JS function with one argument.
88-
*
89-
* Evaluate expr.
90-
* If expr is indexable function (def with at handler), return handler.
91-
* Otherwise, call makeLambda, then return function that set scope
92-
* with one arg, then evaluate result of makeLambda.
93-
*/
94-
95-
export function indexable(
96-
expr: BoxedExpression
97-
): ((index: number) => BoxedExpression | undefined) | undefined {
98-
expr = expr.evaluate();
99-
100-
// If the function expression is indexable (it has an at() handler)
101-
// return the at() handler, bound to this expression.
102-
if (expr.functionDefinition?.at) {
103-
const at = expr.functionDefinition.at;
104-
return (index) => at(expr, index);
105-
}
106-
107-
//
108-
// String at
109-
//
110-
const s = expr.string;
111-
if (s !== null) {
112-
return (index) => {
113-
const c = s.charAt(index);
114-
if (c === undefined) return expr.engine.Nothing;
115-
return expr.engine.string(c);
116-
};
117-
}
118-
119-
// Expressions that don't have an at() handler, have the
120-
// argument applied to them.
121-
const lambda = makeLambda(expr);
122-
if (lambda) return (index) => lambda([expr.engine.number(index)]);
123-
124-
return undefined;
125-
}
126-
12746
/**
12847
* From an expression, return a predicate function, which can be used to filter.
12948
*/

src/compute-engine/latex-syntax/dictionary/definitions-arithmetic.ts

+12-2
Original file line numberDiff line numberDiff line change
@@ -739,10 +739,15 @@ export const DEFINITIONS_ARITHMETIC: LatexDictionary = [
739739
parse: 'Gamma',
740740
},
741741
{
742-
name: 'Gcd',
742+
name: 'GCD',
743743
identifierTrigger: 'gcd',
744744
kind: 'function',
745745
},
746+
{
747+
identifierTrigger: 'GCD',
748+
kind: 'function',
749+
parse: 'GCD',
750+
},
746751
{
747752
name: 'Half',
748753
serialize: '\\frac12',
@@ -792,10 +797,15 @@ export const DEFINITIONS_ARITHMETIC: LatexDictionary = [
792797
},
793798

794799
{
795-
name: 'Lcm',
800+
name: 'LCM',
796801
identifierTrigger: 'lcm',
797802
kind: 'function',
798803
},
804+
{
805+
identifierTrigger: 'LCM',
806+
kind: 'function',
807+
parse: 'LCM',
808+
},
799809
{ identifierTrigger: 'max', kind: 'function', parse: 'Max' },
800810
{ identifierTrigger: 'min', kind: 'function', parse: 'Min' },
801811
{ name: 'Max', latexTrigger: '\\max', kind: 'function' },

0 commit comments

Comments
 (0)