@trebco/treb 27.0.1 → 27.2.1
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-spreadsheet.mjs +8 -8
- package/dist/treb.d.ts +1 -1
- package/package.json +3 -3
- package/treb-calculator/src/calculator.ts +27 -0
- package/treb-calculator/src/functions/finance-functions.ts +185 -0
- package/treb-embed/src/embedded-spreadsheet.ts +4 -10
- package/treb-export/src/export2.ts +10 -10
package/dist/treb.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trebco/treb",
|
|
3
|
-
"version": "27.
|
|
3
|
+
"version": "27.2.1",
|
|
4
4
|
"license": "LGPL-3.0-or-later",
|
|
5
5
|
"homepage": "https://treb.app",
|
|
6
6
|
"repository": {
|
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
"devDependencies": {
|
|
14
14
|
"@types/html-minifier": "^4.0.2",
|
|
15
15
|
"@types/node": "^20.4.0",
|
|
16
|
-
"@typescript-eslint/eslint-plugin": "^
|
|
17
|
-
"@typescript-eslint/parser": "^
|
|
16
|
+
"@typescript-eslint/eslint-plugin": "^6.1.0",
|
|
17
|
+
"@typescript-eslint/parser": "^6.1.0",
|
|
18
18
|
"archiver": "^5.3.0",
|
|
19
19
|
"cssnano": "^6.0.0",
|
|
20
20
|
"esbuild": "^0.18.11",
|
|
@@ -811,6 +811,33 @@ export class Calculator extends Graph {
|
|
|
811
811
|
},
|
|
812
812
|
},
|
|
813
813
|
|
|
814
|
+
|
|
815
|
+
/** not sure when this one appeared, but it's what I was looking for */
|
|
816
|
+
FormulaText: {
|
|
817
|
+
description: 'Returns a formula as a string',
|
|
818
|
+
arguments: [
|
|
819
|
+
{ name: 'reference', description: 'Cell reference', metadata: true, },
|
|
820
|
+
],
|
|
821
|
+
fn: Utilities.ApplyAsArray((reference: UnionValue): UnionValue => {
|
|
822
|
+
|
|
823
|
+
if (!UnionIsMetadata(reference)) {
|
|
824
|
+
return ReferenceError();
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
const sheet = this.model.sheets.Find(reference.value?.address?.sheet_id || 0);
|
|
828
|
+
if (sheet) {
|
|
829
|
+
const cell = sheet.cells.GetCell(reference.value.address, false);
|
|
830
|
+
return {
|
|
831
|
+
type: ValueType.string,
|
|
832
|
+
value: cell?.value?.toString() || '',
|
|
833
|
+
};
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
return ReferenceError();
|
|
837
|
+
|
|
838
|
+
}),
|
|
839
|
+
},
|
|
840
|
+
|
|
814
841
|
/**
|
|
815
842
|
* this should be in the 'information' library but it needs reference
|
|
816
843
|
* to the underlying cell (unresolved)
|
|
@@ -23,6 +23,8 @@ import type { FunctionMap } from '../descriptors';
|
|
|
23
23
|
import { type CellValue, type UnionValue, ValueType } from 'treb-base-types';
|
|
24
24
|
import { FlattenUnboxed } from '../utilities';
|
|
25
25
|
|
|
26
|
+
import { ArgumentError, ReferenceError, UnknownError, ValueError, ExpressionError, NAError, DivideByZeroError } from '../function-error';
|
|
27
|
+
|
|
26
28
|
// use a single, static object for base functions
|
|
27
29
|
|
|
28
30
|
/**
|
|
@@ -114,6 +116,189 @@ export const FinanceFunctionLibrary: FunctionMap = {
|
|
|
114
116
|
}
|
|
115
117
|
},
|
|
116
118
|
|
|
119
|
+
XNPV: {
|
|
120
|
+
description: 'returns the NPV of a nonperiodic stream of payments at a given rate',
|
|
121
|
+
arguments: [
|
|
122
|
+
{ name: 'Discount rate', },
|
|
123
|
+
{ name: 'Values', },
|
|
124
|
+
{ name: 'Dates', },
|
|
125
|
+
],
|
|
126
|
+
fn: (rate: UnionValue, input_values: CellValue[], input_dates: CellValue[]): UnionValue => {
|
|
127
|
+
|
|
128
|
+
if (typeof rate !== 'number') {
|
|
129
|
+
return ArgumentError();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
input_values = FlattenUnboxed(input_values);
|
|
133
|
+
input_dates = FlattenUnboxed(input_dates);
|
|
134
|
+
|
|
135
|
+
// some validation...
|
|
136
|
+
|
|
137
|
+
if (input_values.length !== input_dates.length) {
|
|
138
|
+
return ArgumentError();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const values: number[] = [];
|
|
142
|
+
|
|
143
|
+
for (const value of input_values) {
|
|
144
|
+
if (typeof value !== 'number') {
|
|
145
|
+
return ArgumentError();
|
|
146
|
+
}
|
|
147
|
+
values.push(value);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const dates: number[] = [];
|
|
151
|
+
|
|
152
|
+
//
|
|
153
|
+
// "Numbers in dates are truncated to integers."
|
|
154
|
+
//
|
|
155
|
+
// https://support.microsoft.com/en-gb/office/xirr-function-de1242ec-6477-445b-b11b-a303ad9adc9d
|
|
156
|
+
//
|
|
157
|
+
// what does that mean? rounded? floored? going to assume the latter...
|
|
158
|
+
|
|
159
|
+
for (const date of input_dates) {
|
|
160
|
+
if (typeof date !== 'number') {
|
|
161
|
+
return ArgumentError();
|
|
162
|
+
}
|
|
163
|
+
dates.push(Math.floor(date));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
let npv = 0;
|
|
167
|
+
|
|
168
|
+
for (let j = 0; j < values.length; j++) {
|
|
169
|
+
npv += (values[j] || 0) / Math.pow((1 + rate), (dates[j] - dates[0]) / 365);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
type: ValueType.number,
|
|
174
|
+
value: npv,
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
|
|
180
|
+
XIRR: {
|
|
181
|
+
description: 'returns the internal rate of return of a nonperiodic stream of payments',
|
|
182
|
+
arguments: [
|
|
183
|
+
{ name: 'Values', },
|
|
184
|
+
{ name: 'Dates', },
|
|
185
|
+
{ name: 'Guess', default: .1 },
|
|
186
|
+
],
|
|
187
|
+
fn: (input_values: CellValue[], input_dates: CellValue[], guess = .1): UnionValue => {
|
|
188
|
+
|
|
189
|
+
input_values = FlattenUnboxed(input_values);
|
|
190
|
+
input_dates = FlattenUnboxed(input_dates);
|
|
191
|
+
|
|
192
|
+
// some validation...
|
|
193
|
+
|
|
194
|
+
if (input_values.length !== input_dates.length) {
|
|
195
|
+
return ArgumentError();
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
let positive = 0;
|
|
199
|
+
let negative = 0;
|
|
200
|
+
|
|
201
|
+
const values: number[] = [];
|
|
202
|
+
|
|
203
|
+
for (const value of input_values) {
|
|
204
|
+
if (typeof value !== 'number') {
|
|
205
|
+
// console.info('value not number', value);
|
|
206
|
+
return ArgumentError();
|
|
207
|
+
}
|
|
208
|
+
if (value > 0) { positive++; }
|
|
209
|
+
if (value < 0) { negative++; }
|
|
210
|
+
|
|
211
|
+
values.push(value);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (positive <= 0 || negative <= 0) {
|
|
215
|
+
// console.info('invalid -/+ count', positive, negative);
|
|
216
|
+
return ArgumentError();
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const dates: number[] = [];
|
|
220
|
+
|
|
221
|
+
//
|
|
222
|
+
// "Numbers in dates are truncated to integers."
|
|
223
|
+
//
|
|
224
|
+
// https://support.microsoft.com/en-gb/office/xirr-function-de1242ec-6477-445b-b11b-a303ad9adc9d
|
|
225
|
+
//
|
|
226
|
+
// what does that mean? rounded? floored? going to assume the latter...
|
|
227
|
+
|
|
228
|
+
for (const date of input_dates) {
|
|
229
|
+
if (typeof date !== 'number') {
|
|
230
|
+
return ArgumentError();
|
|
231
|
+
}
|
|
232
|
+
dates.push(Math.floor(date));
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const start = dates[0];
|
|
236
|
+
for (const date of dates) {
|
|
237
|
+
if (date < start) {
|
|
238
|
+
return ArgumentError();
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// per the above reference link we have max steps = 100 and
|
|
243
|
+
// resolution threshold = 1e-8 ("0.000001 percent")
|
|
244
|
+
|
|
245
|
+
const step = .1; // initial step
|
|
246
|
+
|
|
247
|
+
const bounds = [
|
|
248
|
+
{found: false, value: 0},
|
|
249
|
+
{found: false, value: 0},
|
|
250
|
+
];
|
|
251
|
+
|
|
252
|
+
const count = values.length;
|
|
253
|
+
|
|
254
|
+
for (let i = 0; i < 100; i++) {
|
|
255
|
+
|
|
256
|
+
// calculate npv
|
|
257
|
+
let npv = 0;
|
|
258
|
+
|
|
259
|
+
for (let j = 0; j < count; j++) {
|
|
260
|
+
npv += (values[j] || 0) / Math.pow((1 + guess), (dates[j] - dates[0]) / 365);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (Math.abs(npv) <= 1e-6) { // resolution
|
|
264
|
+
// console.info(`** found in ${i + 1} steps`)
|
|
265
|
+
return {
|
|
266
|
+
type: ValueType.number,
|
|
267
|
+
value: guess,
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// search space is unbounded, unfortunately. we can expand exponentially
|
|
272
|
+
// until we have bounds, at which point it's a standard bounded binary search
|
|
273
|
+
|
|
274
|
+
// ...or we can expand linearly, using a reasonable initial step size?
|
|
275
|
+
|
|
276
|
+
if (npv > 0) {
|
|
277
|
+
bounds[0].value = bounds[0].found ? Math.max(bounds[0].value, guess) : guess;
|
|
278
|
+
bounds[0].found = true;
|
|
279
|
+
if (!bounds[1].found) {
|
|
280
|
+
guess += step;
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
else {
|
|
285
|
+
bounds[1].value = bounds[1].found ? Math.min(bounds[1].value, guess) : guess;
|
|
286
|
+
bounds[1].found = true;
|
|
287
|
+
if (!bounds[0].found) {
|
|
288
|
+
guess -= step;
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
guess = bounds[0].value + (bounds[1].value - bounds[0].value) / 2;
|
|
294
|
+
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return ValueError();
|
|
298
|
+
|
|
299
|
+
},
|
|
300
|
+
},
|
|
301
|
+
|
|
117
302
|
IRR: {
|
|
118
303
|
description: 'Calculates the internal rate of return of a series of cashflows',
|
|
119
304
|
arguments: [
|
|
@@ -887,6 +887,9 @@ export class EmbeddedSpreadsheet {
|
|
|
887
887
|
if (data) {
|
|
888
888
|
this.LoadDocument(data as TREBDocument, { recalculate: !!this.options.recalculate, source});
|
|
889
889
|
}
|
|
890
|
+
else {
|
|
891
|
+
this.UpdateDocumentStyles();
|
|
892
|
+
}
|
|
890
893
|
}
|
|
891
894
|
else if (!network_document) {
|
|
892
895
|
|
|
@@ -894,6 +897,7 @@ export class EmbeddedSpreadsheet {
|
|
|
894
897
|
// and the calculator, which would otherwise happen on document load
|
|
895
898
|
|
|
896
899
|
this.calculator.RebuildClean(true);
|
|
900
|
+
this.UpdateDocumentStyles();
|
|
897
901
|
|
|
898
902
|
}
|
|
899
903
|
|
|
@@ -2410,17 +2414,7 @@ export class EmbeddedSpreadsheet {
|
|
|
2410
2414
|
}
|
|
2411
2415
|
|
|
2412
2416
|
if (blob) {
|
|
2413
|
-
|
|
2414
|
-
/*
|
|
2415
|
-
const a = document.createElement('a');
|
|
2416
|
-
a.href = URL.createObjectURL(blob);
|
|
2417
|
-
a.download = filename + '.xlsx';
|
|
2418
|
-
a.click();
|
|
2419
|
-
URL.revokeObjectURL(a.href);
|
|
2420
|
-
// FileSaver.saveAs(blob, filename + '.xlsx', { autoBom: false });
|
|
2421
|
-
*/
|
|
2422
2417
|
this.SaveAs(blob, filename + '.xlsx');
|
|
2423
|
-
|
|
2424
2418
|
this.last_save_version = this.file_version; // even though it's an export, consider it clean
|
|
2425
2419
|
}
|
|
2426
2420
|
|
|
@@ -796,13 +796,13 @@ export class Exporter {
|
|
|
796
796
|
case 'png':
|
|
797
797
|
case 'gif':
|
|
798
798
|
|
|
799
|
-
if (annotation.
|
|
799
|
+
if (annotation.layout) {
|
|
800
800
|
images.push({
|
|
801
|
-
anchor: this.
|
|
801
|
+
anchor: this.AnnotationLayoutToAnchor(annotation.layout, sheet_source), options});
|
|
802
802
|
}
|
|
803
|
-
else if (annotation.
|
|
803
|
+
else if (annotation.rect) {
|
|
804
804
|
images.push({
|
|
805
|
-
anchor: this.
|
|
805
|
+
anchor: this.AnnotationRectToAnchor(annotation.rect, sheet_source), options});
|
|
806
806
|
}
|
|
807
807
|
else {
|
|
808
808
|
console.warn('annotation missing layout');
|
|
@@ -1006,16 +1006,16 @@ export class Exporter {
|
|
|
1006
1006
|
|
|
1007
1007
|
const rect = (annotation as AnnotationData & { rect?: Partial<Rectangle>}).rect;
|
|
1008
1008
|
|
|
1009
|
-
if (
|
|
1010
|
-
charts.push({
|
|
1011
|
-
anchor: this.AnnotationRectToAnchor(rect, sheet_source), options});
|
|
1012
|
-
// sheet.AddChart(this.AnnotationRectToAnchor(annotation.rect, sheet_source), options);
|
|
1013
|
-
}
|
|
1014
|
-
else if (annotation.layout) {
|
|
1009
|
+
if (annotation.layout) {
|
|
1015
1010
|
charts.push({
|
|
1016
1011
|
anchor: this.AnnotationLayoutToAnchor(annotation.layout, sheet_source), options});
|
|
1017
1012
|
// sheet.AddChart(this.AnnotationLayoutToAnchor(annotation.layout, sheet_source), options);
|
|
1018
1013
|
}
|
|
1014
|
+
else if (rect) {
|
|
1015
|
+
charts.push({
|
|
1016
|
+
anchor: this.AnnotationRectToAnchor(rect, sheet_source), options});
|
|
1017
|
+
// sheet.AddChart(this.AnnotationRectToAnchor(annotation.rect, sheet_source), options);
|
|
1018
|
+
}
|
|
1019
1019
|
else {
|
|
1020
1020
|
console.warn('annotation missing layout');
|
|
1021
1021
|
}
|