@trebco/treb 27.1.2 → 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.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- /*! API v27.1. Copyright 2018-2023 trebco, llc. All rights reserved. LGPL: https://treb.app/license */
1
+ /*! API v27.2. Copyright 2018-2023 trebco, llc. All rights reserved. LGPL: https://treb.app/license */
2
2
 
3
3
  /**
4
4
  * add our tag to the map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trebco/treb",
3
- "version": "27.1.2",
3
+ "version": "27.2.1",
4
4
  "license": "LGPL-3.0-or-later",
5
5
  "homepage": "https://treb.app",
6
6
  "repository": {
@@ -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: [
@@ -796,13 +796,13 @@ export class Exporter {
796
796
  case 'png':
797
797
  case 'gif':
798
798
 
799
- if (annotation.rect) {
799
+ if (annotation.layout) {
800
800
  images.push({
801
- anchor: this.AnnotationRectToAnchor(annotation.rect, sheet_source), options});
801
+ anchor: this.AnnotationLayoutToAnchor(annotation.layout, sheet_source), options});
802
802
  }
803
- else if (annotation.layout) {
803
+ else if (annotation.rect) {
804
804
  images.push({
805
- anchor: this.AnnotationLayoutToAnchor(annotation.layout, sheet_source), options});
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 (rect) {
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
  }