@trebco/treb 32.8.1 → 32.9.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/treb-export-worker.mjs +2 -2
- package/dist/treb-spreadsheet.mjs +12 -12
- package/dist/treb.d.ts +1 -1
- package/package.json +1 -1
- package/treb-calculator/src/calculator.ts +111 -35
- package/treb-calculator/src/functions/beta.ts +15 -1
- package/treb-calculator/src/functions/normal.ts +37 -0
- package/treb-calculator/src/functions/statistics-functions.ts +69 -16
- package/treb-calculator/src/functions/students-t.ts +85 -0
- package/treb-parser/src/parser.ts +17 -7
package/dist/treb.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -1014,65 +1014,141 @@ export class Calculator extends Graph {
|
|
|
1014
1014
|
|
|
1015
1015
|
/**
|
|
1016
1016
|
* FIXME: there are cases we are not handling
|
|
1017
|
+
*
|
|
1018
|
+
* update to return a reference so you can use it as part of a
|
|
1019
|
+
* range. we're not handling literal arrays atm.
|
|
1017
1020
|
*/
|
|
1018
1021
|
Index: {
|
|
1022
|
+
return_type: 'reference',
|
|
1019
1023
|
arguments: [
|
|
1020
|
-
{ name: 'range',
|
|
1024
|
+
{ name: 'range', metadata: true, },
|
|
1021
1025
|
{ name: 'row', },
|
|
1022
1026
|
{ name: 'column', }
|
|
1023
1027
|
],
|
|
1028
|
+
|
|
1029
|
+
// volatile: true, // not sure this is necessary bc input is the range
|
|
1024
1030
|
volatile: false,
|
|
1025
1031
|
|
|
1026
1032
|
// FIXME: handle full row, full column calls
|
|
1027
|
-
fn: (
|
|
1033
|
+
fn: (range: UnionValue, row: number|undefined, column: number|undefined) => {
|
|
1028
1034
|
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
+
if (!range) {
|
|
1036
|
+
return ArgumentError();
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
// this is illegal, although we could just default to zeros
|
|
1040
|
+
|
|
1041
|
+
if (row === undefined && column === undefined) {
|
|
1042
|
+
return ArgumentError();
|
|
1035
1043
|
}
|
|
1036
1044
|
|
|
1037
|
-
|
|
1045
|
+
row = row || 0;
|
|
1046
|
+
column = column || 0;
|
|
1038
1047
|
|
|
1039
|
-
|
|
1048
|
+
let arr: { value: {address: UnitAddress }}[][] = [];
|
|
1049
|
+
|
|
1050
|
+
// NOTE: could be a single cell. in that case it won't be passed
|
|
1051
|
+
// as an array so we'll need a second branch (or convert it)
|
|
1052
|
+
|
|
1053
|
+
if (range.type === ValueType.array) {
|
|
1054
|
+
|
|
1055
|
+
// FIXME: validate these are addresses (shouldn't be necessary
|
|
1056
|
+
// if we're marking the argument as metadata? what about literals?)
|
|
1057
|
+
|
|
1058
|
+
// check rows and columns. we might need to return an array.
|
|
1059
|
+
|
|
1060
|
+
arr = range.value as unknown as { value: {address: UnitAddress }}[][];
|
|
1040
1061
|
|
|
1041
|
-
const c = data.value[column - 1];
|
|
1042
|
-
if (c) {
|
|
1043
|
-
const cell = c[row - 1];
|
|
1044
|
-
if (cell) {
|
|
1045
|
-
return cell;
|
|
1046
|
-
}
|
|
1047
|
-
}
|
|
1048
1062
|
}
|
|
1049
|
-
else if (
|
|
1063
|
+
else if (range.type === ValueType.object) {
|
|
1050
1064
|
|
|
1051
|
-
|
|
1065
|
+
const metadata = range.value as { type: string, address?: UnitAddress };
|
|
1052
1066
|
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
if (!c[row - 1]) {
|
|
1056
|
-
return ArgumentError();
|
|
1057
|
-
}
|
|
1058
|
-
value.push([c[row-1]]);
|
|
1067
|
+
if (metadata.type === 'metadata' && metadata.address) {
|
|
1068
|
+
arr.push([range as { value: { address: UnitAddress }}]);
|
|
1059
1069
|
}
|
|
1070
|
+
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
const columns = arr.length;
|
|
1074
|
+
const rows = arr[0]?.length || 0;
|
|
1075
|
+
|
|
1076
|
+
if (rows <= 0 || columns <= 0 || row > rows || column > columns || row < 0 || column < 0) {
|
|
1077
|
+
return ArgumentError();
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
|
|
1081
|
+
// return everything if arguments are (0, 0)
|
|
1082
|
+
|
|
1083
|
+
if (column === 0 && row === 0) {
|
|
1084
|
+
|
|
1085
|
+
// because of the way we're structured this we might be
|
|
1086
|
+
// returning a range of length 1; in that case we want
|
|
1087
|
+
// to return it as an address
|
|
1088
|
+
|
|
1089
|
+
const expression: ExpressionUnit = (columns === 1 && rows === 1) ? {
|
|
1090
|
+
...arr[0][0].value.address,
|
|
1091
|
+
} : {
|
|
1092
|
+
type: 'range',
|
|
1093
|
+
start: arr[0][0].value.address,
|
|
1094
|
+
end: arr[columns-1][rows-1].value.address,
|
|
1095
|
+
label: '', position: 0,
|
|
1096
|
+
id: 0,
|
|
1097
|
+
};
|
|
1098
|
+
|
|
1060
1099
|
return {
|
|
1061
|
-
type: ValueType.
|
|
1062
|
-
value,
|
|
1100
|
+
type: ValueType.object,
|
|
1101
|
+
value: expression,
|
|
1102
|
+
};
|
|
1103
|
+
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
// single cell
|
|
1107
|
+
|
|
1108
|
+
if ((row || rows === 1) && (column || columns === 1)) {
|
|
1109
|
+
return {
|
|
1110
|
+
type: ValueType.object,
|
|
1111
|
+
value: arr[column ? column - 1 : 0][row ? row - 1 : 0].value.address,
|
|
1063
1112
|
};
|
|
1064
1113
|
}
|
|
1114
|
+
|
|
1115
|
+
// sub array
|
|
1116
|
+
|
|
1117
|
+
if (row) {
|
|
1118
|
+
|
|
1119
|
+
// return column
|
|
1120
|
+
|
|
1121
|
+
const expression: ExpressionUnit = {
|
|
1122
|
+
type: 'range',
|
|
1123
|
+
start: arr[0][row - 1].value.address,
|
|
1124
|
+
end: arr[columns-1][row-1].value.address,
|
|
1125
|
+
label: '', position: 0,
|
|
1126
|
+
id: 0,
|
|
1127
|
+
};
|
|
1128
|
+
|
|
1129
|
+
return {
|
|
1130
|
+
type: ValueType.object,
|
|
1131
|
+
value: expression,
|
|
1132
|
+
};
|
|
1133
|
+
|
|
1134
|
+
}
|
|
1065
1135
|
else if (column) {
|
|
1066
1136
|
|
|
1067
|
-
// return
|
|
1137
|
+
// return row
|
|
1138
|
+
|
|
1139
|
+
const expression: ExpressionUnit = {
|
|
1140
|
+
type: 'range',
|
|
1141
|
+
start: arr[column - 1][0].value.address,
|
|
1142
|
+
end: arr[column-1][rows-1].value.address,
|
|
1143
|
+
label: '', position: 0,
|
|
1144
|
+
id: 0,
|
|
1145
|
+
};
|
|
1146
|
+
|
|
1147
|
+
return {
|
|
1148
|
+
type: ValueType.object,
|
|
1149
|
+
value: expression,
|
|
1150
|
+
};
|
|
1068
1151
|
|
|
1069
|
-
const c = data.value[column - 1];
|
|
1070
|
-
if (c) {
|
|
1071
|
-
return {
|
|
1072
|
-
type: ValueType.array,
|
|
1073
|
-
value: [c],
|
|
1074
|
-
};
|
|
1075
|
-
}
|
|
1076
1152
|
}
|
|
1077
1153
|
|
|
1078
1154
|
return ArgumentError();
|
|
@@ -32,7 +32,7 @@ const Log1p = (x: number) => {
|
|
|
32
32
|
/**
|
|
33
33
|
* continued fraction expansion
|
|
34
34
|
*/
|
|
35
|
-
const BetaContFrac = (a: number, b: number, x: number, epsabs: number) => {
|
|
35
|
+
export const BetaContFrac = (a: number, b: number, x: number, epsabs: number) => {
|
|
36
36
|
|
|
37
37
|
const cutoff = 2.0 * Number.MIN_VALUE;
|
|
38
38
|
|
|
@@ -242,3 +242,17 @@ export const BetaCDF = (x: number, a: number, b: number) => {
|
|
|
242
242
|
|
|
243
243
|
};
|
|
244
244
|
|
|
245
|
+
export const BetaInc = (x: number, a: number, b: number): number => {
|
|
246
|
+
if (x < 0 || x > 1) return 0; // throw new Error("Invalid x in betainc");
|
|
247
|
+
const bt =
|
|
248
|
+
x === 0 || x === 1
|
|
249
|
+
? 0
|
|
250
|
+
: Math.exp(LnGamma(a + b) - LnGamma(a) - LnGamma(b) + a * Math.log(x) + b * Math.log(1 - x));
|
|
251
|
+
if (x < (a + 1) / (a + b + 2)) {
|
|
252
|
+
return bt * BetaContFrac(a, b, x, 0) / a;
|
|
253
|
+
} else {
|
|
254
|
+
return 1 - bt * BetaContFrac(b, a, 1 - x, 0) / b;
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
|
|
2
|
+
/*
|
|
3
|
+
* This file is part of TREB.
|
|
4
|
+
*
|
|
5
|
+
* TREB is free software: you can redistribute it and/or modify it under the
|
|
6
|
+
* terms of the GNU General Public License as published by the Free Software
|
|
7
|
+
* Foundation, either version 3 of the License, or (at your option) any
|
|
8
|
+
* later version.
|
|
9
|
+
*
|
|
10
|
+
* TREB is distributed in the hope that it will be useful, but WITHOUT ANY
|
|
11
|
+
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
|
12
|
+
* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
|
13
|
+
* details.
|
|
14
|
+
*
|
|
15
|
+
* You should have received a copy of the GNU General Public License along
|
|
16
|
+
* with TREB. If not, see <https://www.gnu.org/licenses/>.
|
|
17
|
+
*
|
|
18
|
+
* Copyright 2022-2025 trebco, llc.
|
|
19
|
+
* info@treb.app
|
|
20
|
+
*
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/** imprecise but reasonably fast normsinv function */
|
|
24
|
+
export const InverseNormal = (q: number): number => {
|
|
25
|
+
|
|
26
|
+
if (q === 0.50) {
|
|
27
|
+
return 0;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const p = (q < 1.0 && q > 0.5) ? (1 - q) : q;
|
|
31
|
+
const t = Math.sqrt(Math.log(1.0 / Math.pow(p, 2.0)));
|
|
32
|
+
const x = t - (2.515517 + 0.802853 * t + 0.010328 * Math.pow(t, 2.0)) /
|
|
33
|
+
(1.0 + 1.432788 * t + 0.189269 * Math.pow(t, 2.0) + 0.001308 * Math.pow(t, 3.0));
|
|
34
|
+
|
|
35
|
+
return (q > 0.5 ? x : -x);
|
|
36
|
+
|
|
37
|
+
};
|
|
@@ -27,6 +27,8 @@ import * as ComplexMath from '../complex-math';
|
|
|
27
27
|
|
|
28
28
|
import { BetaCDF, BetaPDF, InverseBeta, LnGamma } from './beta';
|
|
29
29
|
import { gamma_p } from './gamma';
|
|
30
|
+
import { InverseNormal } from './normal';
|
|
31
|
+
import { tCDF, tInverse, tPDF } from './students-t';
|
|
30
32
|
|
|
31
33
|
/** error function (for gaussian distribution) */
|
|
32
34
|
const erf = (x: number): number => {
|
|
@@ -62,21 +64,7 @@ const norm_dist = (x: number, mean: number, stdev: number, cumulative: boolean)
|
|
|
62
64
|
|
|
63
65
|
}
|
|
64
66
|
|
|
65
|
-
/** imprecise but reasonably fast normsinv function */
|
|
66
|
-
const inverse_normal = (q: number): number => {
|
|
67
67
|
|
|
68
|
-
if (q === 0.50) {
|
|
69
|
-
return 0;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const p = (q < 1.0 && q > 0.5) ? (1 - q) : q;
|
|
73
|
-
const t = Math.sqrt(Math.log(1.0 / Math.pow(p, 2.0)));
|
|
74
|
-
const x = t - (2.515517 + 0.802853 * t + 0.010328 * Math.pow(t, 2.0)) /
|
|
75
|
-
(1.0 + 1.432788 * t + 0.189269 * Math.pow(t, 2.0) + 0.001308 * Math.pow(t, 3.0));
|
|
76
|
-
|
|
77
|
-
return (q > 0.5 ? x : -x);
|
|
78
|
-
|
|
79
|
-
};
|
|
80
68
|
|
|
81
69
|
const Median = (data: number[]) => {
|
|
82
70
|
const n = data.length;
|
|
@@ -478,7 +466,7 @@ export const StatisticsFunctionLibrary: FunctionMap = {
|
|
|
478
466
|
fn: (q: number, mean = 0, stdev = 1): UnionValue => {
|
|
479
467
|
return {
|
|
480
468
|
type: ValueType.number,
|
|
481
|
-
value:
|
|
469
|
+
value: InverseNormal(q) * stdev + mean,
|
|
482
470
|
}
|
|
483
471
|
}
|
|
484
472
|
},
|
|
@@ -492,7 +480,7 @@ export const StatisticsFunctionLibrary: FunctionMap = {
|
|
|
492
480
|
fn: (q: number): UnionValue => {
|
|
493
481
|
return {
|
|
494
482
|
type: ValueType.number,
|
|
495
|
-
value:
|
|
483
|
+
value: InverseNormal(q),
|
|
496
484
|
}
|
|
497
485
|
}
|
|
498
486
|
},
|
|
@@ -831,6 +819,71 @@ export const StatisticsFunctionLibrary: FunctionMap = {
|
|
|
831
819
|
}
|
|
832
820
|
},
|
|
833
821
|
|
|
822
|
+
'T.DIST': {
|
|
823
|
+
description: `Returns the left-tailed Student's t-distribution`,
|
|
824
|
+
arguments: [
|
|
825
|
+
{ name: 'X', unroll: true, boxed: true, },
|
|
826
|
+
{ name: 'degrees of freedom', unroll: true, boxed: true, },
|
|
827
|
+
{ name: 'cumulative', unroll: true, boxed: true, },
|
|
828
|
+
],
|
|
829
|
+
fn: (x: UnionValue, df: UnionValue, cumulative?: UnionValue) => {
|
|
830
|
+
|
|
831
|
+
const cum = cumulative ? !!cumulative.value : false;
|
|
832
|
+
|
|
833
|
+
if (df.type !== ValueType.number || x.type !== ValueType.number || df.value < 1) {
|
|
834
|
+
return ArgumentError();
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
return {
|
|
838
|
+
type: ValueType.number,
|
|
839
|
+
value: cum ? tCDF(x.value, df.value) : tPDF(x.value, df.value),
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
},
|
|
843
|
+
},
|
|
844
|
+
|
|
845
|
+
'T.Inv': {
|
|
846
|
+
description: `Returns the left-tailed inverse of the Student's t-distribution`,
|
|
847
|
+
arguments: [
|
|
848
|
+
{
|
|
849
|
+
name: 'Probability', boxed: true, unroll: true,
|
|
850
|
+
},
|
|
851
|
+
{
|
|
852
|
+
name: 'Degrees of freedom', boxed: true, unroll: true,
|
|
853
|
+
},
|
|
854
|
+
],
|
|
855
|
+
fn: (p: UnionValue, df: UnionValue) => {
|
|
856
|
+
if (df.type !== ValueType.number || df.value < 1 || p.type !== ValueType.number || p.value <= 0 || p.value >= 1) {
|
|
857
|
+
return ArgumentError();
|
|
858
|
+
}
|
|
859
|
+
return {
|
|
860
|
+
type: ValueType.number,
|
|
861
|
+
value: tInverse(p.value, df.value),
|
|
862
|
+
};
|
|
863
|
+
},
|
|
864
|
+
},
|
|
865
|
+
|
|
866
|
+
'T.Inv.2T': {
|
|
867
|
+
description: `Returns the two-tailed inverse of the Student's t-distribution`,
|
|
868
|
+
arguments: [
|
|
869
|
+
{
|
|
870
|
+
name: 'Probability', boxed: true, unroll: true,
|
|
871
|
+
},
|
|
872
|
+
{
|
|
873
|
+
name: 'Degrees of freedom', boxed: true, unroll: true,
|
|
874
|
+
},
|
|
875
|
+
],
|
|
876
|
+
fn: (p: UnionValue, df: UnionValue) => {
|
|
877
|
+
if (df.type !== ValueType.number || df.value < 1 ||p.type !== ValueType.number || p.value <= 0 || p.value >= 1) {
|
|
878
|
+
return ArgumentError();
|
|
879
|
+
}
|
|
880
|
+
return {
|
|
881
|
+
type: ValueType.number,
|
|
882
|
+
value: Math.abs(tInverse(1 - p.value/2, df.value)),
|
|
883
|
+
};
|
|
884
|
+
},
|
|
885
|
+
},
|
|
886
|
+
|
|
834
887
|
GammaLn: {
|
|
835
888
|
description: 'Returns the natural log of the gamma function',
|
|
836
889
|
arguments: [{ name: 'value', boxed: true, unroll: true }],
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This file is part of TREB.
|
|
3
|
+
*
|
|
4
|
+
* TREB is free software: you can redistribute it and/or modify it under the
|
|
5
|
+
* terms of the GNU General Public License as published by the Free Software
|
|
6
|
+
* Foundation, either version 3 of the License, or (at your option) any
|
|
7
|
+
* later version.
|
|
8
|
+
*
|
|
9
|
+
* TREB is distributed in the hope that it will be useful, but WITHOUT ANY
|
|
10
|
+
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
|
11
|
+
* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
|
12
|
+
* details.
|
|
13
|
+
*
|
|
14
|
+
* You should have received a copy of the GNU General Public License along
|
|
15
|
+
* with TREB. If not, see <https://www.gnu.org/licenses/>.
|
|
16
|
+
*
|
|
17
|
+
* Copyright 2022-2025 trebco, llc.
|
|
18
|
+
* info@treb.app
|
|
19
|
+
*
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { BetaInc, LnGamma } from './beta';
|
|
23
|
+
import { InverseNormal } from './normal';
|
|
24
|
+
|
|
25
|
+
export const tCDF = (t: number, df: number) => {
|
|
26
|
+
const x = df / (df + t * t);
|
|
27
|
+
const a = df / 2;
|
|
28
|
+
const b = 0.5;
|
|
29
|
+
const ib = BetaInc(x, a, b);
|
|
30
|
+
return t >= 0 ? 1 - 0.5 * ib : 0.5 * ib;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const tPDF = (t: number, nu: number): number => {
|
|
34
|
+
|
|
35
|
+
const logNumerator = LnGamma((nu + 1) / 2);
|
|
36
|
+
const logDenominator = LnGamma(nu / 2) + 0.5 * Math.log(nu * Math.PI);
|
|
37
|
+
const logFactor = -((nu + 1) / 2) * Math.log(1 + (t * t) / nu);
|
|
38
|
+
|
|
39
|
+
const logPDF = logNumerator - logDenominator + logFactor;
|
|
40
|
+
|
|
41
|
+
return Math.exp(logPDF);
|
|
42
|
+
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const tInverse = (p: number, df: number) => {
|
|
46
|
+
|
|
47
|
+
const EPS = 1e-12;
|
|
48
|
+
|
|
49
|
+
let x = 1;
|
|
50
|
+
|
|
51
|
+
// Hill's approximation for initial guess
|
|
52
|
+
|
|
53
|
+
{
|
|
54
|
+
const t = InverseNormal(p);
|
|
55
|
+
const g1 = (Math.pow(t, 3) + t) / 4;
|
|
56
|
+
const g2 = ((5 * Math.pow(t, 5)) + (16 * Math.pow(t, 3)) + (3 * t)) / 96;
|
|
57
|
+
const g3 = ((3 * Math.pow(t, 7)) + (19 * Math.pow(t, 5)) + (17 * Math.pow(t, 3)) - 15 * t) / 384;
|
|
58
|
+
const g4 = ((79 * Math.pow(t, 9)) + (776 * Math.pow(t, 7)) + (1482 * Math.pow(t, 5)) - (1920 * Math.pow(t, 3)) - (945 * t)) / 92160;
|
|
59
|
+
|
|
60
|
+
x = t + g1 / df + g2 / Math.pow(df, 2) + g3 / Math.pow(df, 3) + g4 / Math.pow(df, 4);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
for (let i = 0; i < 16; i++) {
|
|
64
|
+
|
|
65
|
+
const error = tCDF(x, df) - p;
|
|
66
|
+
if (Math.abs(error) < EPS) {
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const derivative = tPDF(x, df);
|
|
71
|
+
if (derivative === 0) {
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const delta = error / derivative;
|
|
76
|
+
const step = Math.max(-1, Math.min(1, delta));
|
|
77
|
+
|
|
78
|
+
x -= step;
|
|
79
|
+
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return x;
|
|
83
|
+
|
|
84
|
+
};
|
|
85
|
+
|
|
@@ -630,13 +630,23 @@ export class Parser {
|
|
|
630
630
|
}).join(', ')).join('; ') + '}';
|
|
631
631
|
|
|
632
632
|
case 'binary':
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
633
|
+
|
|
634
|
+
// in some cases we might see range constructs as binary units
|
|
635
|
+
// because one side (or maybe both sides) of the range is a
|
|
636
|
+
// function. in that case we don't want a space in front of the
|
|
637
|
+
// operator.
|
|
638
|
+
|
|
639
|
+
{
|
|
640
|
+
const separator = (unit.operator === ':' ? '': ' ');
|
|
641
|
+
|
|
642
|
+
return (
|
|
643
|
+
this.Render(unit.left, options) +
|
|
644
|
+
separator +
|
|
645
|
+
unit.operator +
|
|
646
|
+
separator +
|
|
647
|
+
this.Render(unit.right, options)
|
|
648
|
+
);
|
|
649
|
+
}
|
|
640
650
|
|
|
641
651
|
case 'unary':
|
|
642
652
|
return (
|