@trebco/treb 29.3.3 → 29.4.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-light.mjs +11 -11
- package/dist/treb-spreadsheet.mjs +11 -11
- package/dist/treb.d.ts +3 -25
- package/package.json +1 -1
- package/treb-base-types/src/area.ts +25 -1
- package/treb-base-types/src/cell.ts +2 -46
- package/treb-base-types/src/cells.ts +14 -8
- package/treb-base-types/src/import.ts +2 -2
- package/treb-calculator/src/calculator.ts +22 -12
- package/treb-calculator/src/dag/graph.ts +12 -3
- package/treb-calculator/src/expression-calculator.ts +66 -74
- package/treb-calculator/src/functions/base-functions.ts +2 -2
- package/treb-calculator/src/functions/statistics-functions.ts +31 -1
- package/treb-data-model/src/data-validation.ts +44 -0
- package/treb-data-model/src/data_model.ts +12 -8
- package/treb-data-model/src/index.ts +1 -1
- package/treb-data-model/src/named.ts +35 -10
- package/treb-data-model/src/sheet.ts +61 -0
- package/treb-data-model/src/sheet_types.ts +4 -0
- package/treb-embed/src/embedded-spreadsheet.ts +21 -15
- package/treb-embed/src/progress-dialog.ts +4 -1
- package/treb-export/src/export2.ts +27 -14
- package/treb-export/src/import2.ts +58 -12
- package/treb-grid/src/layout/base_layout.ts +2 -11
- package/treb-grid/src/types/grid.ts +29 -18
- package/treb-grid/src/types/grid_base.ts +83 -16
- package/treb-grid/src/types/grid_command.ts +2 -1
- package/treb-parser/src/parser.ts +22 -4
|
@@ -27,7 +27,7 @@ import type { Cell, ICellAddress,
|
|
|
27
27
|
UndefinedUnion,
|
|
28
28
|
ComplexUnion,
|
|
29
29
|
DimensionedQuantityUnion} from 'treb-base-types';
|
|
30
|
-
import { ValueType, GetValueType } from 'treb-base-types';
|
|
30
|
+
import { ValueType, GetValueType, Area } from 'treb-base-types';
|
|
31
31
|
import type { Parser, ExpressionUnit, UnitBinary, UnitIdentifier,
|
|
32
32
|
UnitGroup, UnitUnary, UnitAddress, UnitRange, UnitCall, UnitDimensionedQuantity, UnitStructuredReference } from 'treb-parser';
|
|
33
33
|
import type { DataModel, MacroFunction, Sheet } from 'treb-data-model';
|
|
@@ -36,8 +36,26 @@ import { ReturnType } from './descriptors';
|
|
|
36
36
|
|
|
37
37
|
import * as Primitives from './primitives';
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
//////////
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* dynamically adding a user data field to the expression so we can
|
|
43
|
+
* cache a function call, avoiding type switching and function lookups.
|
|
44
|
+
*
|
|
45
|
+
* we use the generic type so we can cover the composite type as well
|
|
46
|
+
* before specifying
|
|
47
|
+
*/
|
|
48
|
+
type AttachCachedFunction<T> = (T & { fn: (arg0: T) => UnionValue });
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* expression unit with cached function
|
|
52
|
+
*/
|
|
53
|
+
type ExpressionWithCachedFunction<T extends ExpressionUnit> = T extends { type: T['type'] } ? AttachCachedFunction<T> : never;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* @internal
|
|
57
|
+
*/
|
|
58
|
+
export type ExtendedExpressionUnit = ExpressionWithCachedFunction<ExpressionUnit>;
|
|
41
59
|
|
|
42
60
|
// FIXME: move
|
|
43
61
|
export const UnionIsExpressionUnit = (test: UnionValue /*UnionOrArray*/): test is { type: ValueType.object, value: ExpressionUnit } => {
|
|
@@ -61,16 +79,18 @@ export const UnionIsMetadata = (test: UnionValue /*UnionOrArray*/): test is { ty
|
|
|
61
79
|
// FIXME: move
|
|
62
80
|
export interface ReferenceMetadata {
|
|
63
81
|
type: 'metadata';
|
|
82
|
+
|
|
83
|
+
// what's the context in which I was using the unit address (parse expression?)
|
|
64
84
|
address: UnitAddress; // ICellAddress;
|
|
85
|
+
|
|
65
86
|
value: CellValue;
|
|
66
87
|
format?: string;
|
|
67
88
|
}
|
|
68
89
|
|
|
69
90
|
export interface CalculationContext {
|
|
70
91
|
address: ICellAddress;
|
|
71
|
-
model?: DataModel;
|
|
92
|
+
// model?: DataModel;
|
|
72
93
|
volatile: boolean;
|
|
73
|
-
call_index: number;
|
|
74
94
|
}
|
|
75
95
|
|
|
76
96
|
export class ExpressionCalculator {
|
|
@@ -78,57 +98,30 @@ export class ExpressionCalculator {
|
|
|
78
98
|
public context: CalculationContext = {
|
|
79
99
|
address: { row: -1, column: -1 },
|
|
80
100
|
volatile: false,
|
|
81
|
-
call_index: 0,
|
|
82
101
|
};
|
|
83
102
|
|
|
84
|
-
/**
|
|
85
|
-
* this refers to the number of function call within a single cell.
|
|
86
|
-
* so if you have a function like
|
|
87
|
-
*
|
|
88
|
-
* =A(B())
|
|
89
|
-
*
|
|
90
|
-
* then when calculating A call index should be set to 1; and when
|
|
91
|
-
* calculating B, call index is 2. and so on.
|
|
92
|
-
*/
|
|
93
|
-
protected call_index = 0;
|
|
94
|
-
|
|
95
|
-
// local reference
|
|
96
|
-
// protected cells: Cells = new Cells();
|
|
97
|
-
// protected cells_map: {[index: number]: Cells} = {};
|
|
98
|
-
// protected sheet_name_map: {[index: string]: number} = {};
|
|
99
|
-
|
|
100
|
-
// local reference
|
|
101
|
-
// protected named_range_map: {[index: string]: Area} = {};
|
|
102
|
-
|
|
103
|
-
// protected bound_name_stack: Array<Record<string, ExpressionUnit>> = [];
|
|
104
|
-
|
|
105
103
|
//
|
|
106
|
-
protected data_model!: DataModel;
|
|
104
|
+
// protected data_model!: DataModel; // can we set in ctor? I think this is a legacy hack
|
|
107
105
|
|
|
108
106
|
|
|
109
107
|
// --- public API -----------------------------------------------------------
|
|
110
108
|
|
|
111
109
|
constructor(
|
|
110
|
+
protected readonly data_model: DataModel,
|
|
112
111
|
protected readonly library: FunctionLibrary,
|
|
113
112
|
protected readonly parser: Parser) {}
|
|
114
113
|
|
|
114
|
+
/*
|
|
115
115
|
public SetModel(model: DataModel): void {
|
|
116
116
|
|
|
117
|
-
// this
|
|
118
|
-
//
|
|
119
|
-
|
|
120
|
-
/*
|
|
121
|
-
for (const sheet of model.sheets.list) {
|
|
122
|
-
// this.cells_map[sheet.id] = sheet.cells;
|
|
123
|
-
// this.sheet_name_map[sheet.name.toLowerCase()] = sheet.id;
|
|
124
|
-
}
|
|
125
|
-
*/
|
|
117
|
+
// is this kept around for some side-effects or something? does
|
|
118
|
+
// the model ever change?
|
|
126
119
|
|
|
127
120
|
this.data_model = model;
|
|
128
|
-
// this.named_range_map = model.named_ranges.Map();
|
|
129
121
|
this.context.model = model;
|
|
130
122
|
|
|
131
123
|
}
|
|
124
|
+
*/
|
|
132
125
|
|
|
133
126
|
/**
|
|
134
127
|
* there's a case where we are calling this from within a function
|
|
@@ -141,10 +134,6 @@ export class ExpressionCalculator {
|
|
|
141
134
|
|
|
142
135
|
this.context.address = addr;
|
|
143
136
|
this.context.volatile = false;
|
|
144
|
-
this.context.call_index = 0;
|
|
145
|
-
|
|
146
|
-
// reset for this cell
|
|
147
|
-
this.call_index = 0; // why not in model? A: timing (nested)
|
|
148
137
|
|
|
149
138
|
}
|
|
150
139
|
|
|
@@ -218,8 +207,7 @@ export class ExpressionCalculator {
|
|
|
218
207
|
}
|
|
219
208
|
|
|
220
209
|
/** breaking this out to de-dupe */
|
|
221
|
-
|
|
222
|
-
protected GetMetadata(arg: ExpressionUnit, map_result: (cell_data: Cell, address: ICellAddress) => any): UnionValue /*UnionOrArray*/ {
|
|
210
|
+
protected GetMetadata<T>(arg: ExpressionUnit, transform: (cell_data: Cell, address: ICellAddress) => T): UnionValue {
|
|
223
211
|
|
|
224
212
|
// FIXME: we used to restrict this to non-cell functions, now
|
|
225
213
|
// we are using it for the cell function (we used to use address,
|
|
@@ -328,10 +316,24 @@ export class ExpressionCalculator {
|
|
|
328
316
|
|
|
329
317
|
const metadata: ReferenceMetadata = {
|
|
330
318
|
type: 'metadata',
|
|
331
|
-
|
|
319
|
+
|
|
320
|
+
// metadata is expecting a parse expression instead of an addresss.
|
|
321
|
+
// note we're not setting the label properly here, which could be
|
|
322
|
+
// an issue? not sure who's calling it in this case
|
|
323
|
+
|
|
324
|
+
// UPDATE: "Cell" is calling it, so it needs a label
|
|
325
|
+
|
|
326
|
+
address: {
|
|
327
|
+
...address,
|
|
328
|
+
position: 0,
|
|
329
|
+
id: 0,
|
|
330
|
+
type: 'address',
|
|
331
|
+
label: new Area(address).spreadsheet_label,
|
|
332
|
+
},
|
|
333
|
+
|
|
332
334
|
value,
|
|
333
335
|
format: cell_data.style ? cell_data.style.number_format : undefined,
|
|
334
|
-
...
|
|
336
|
+
...transform(cell_data, address),
|
|
335
337
|
};
|
|
336
338
|
|
|
337
339
|
return { type: ValueType.object, value: metadata, key: 'metadata' };
|
|
@@ -378,7 +380,7 @@ export class ExpressionCalculator {
|
|
|
378
380
|
address,
|
|
379
381
|
value,
|
|
380
382
|
format: cell_data.style ? cell_data.style.number_format : undefined,
|
|
381
|
-
...
|
|
383
|
+
...transform(cell_data, address),
|
|
382
384
|
};
|
|
383
385
|
|
|
384
386
|
column_result.push({
|
|
@@ -511,11 +513,6 @@ export class ExpressionCalculator {
|
|
|
511
513
|
|
|
512
514
|
return (expr: UnitCall) => {
|
|
513
515
|
|
|
514
|
-
// get an index we can use for this call (we may recurse when
|
|
515
|
-
// calculating arguments), then increment for the next call.
|
|
516
|
-
|
|
517
|
-
const call_index = this.call_index++;
|
|
518
|
-
|
|
519
516
|
// yeah so this is clear. just checking volatile.
|
|
520
517
|
|
|
521
518
|
// FIXME: should this be set later, at the same time as the
|
|
@@ -656,11 +653,6 @@ export class ExpressionCalculator {
|
|
|
656
653
|
return argument_error;
|
|
657
654
|
}
|
|
658
655
|
|
|
659
|
-
// if we have any nested calls, they may have updated the index so
|
|
660
|
-
// we use the captured value here.
|
|
661
|
-
|
|
662
|
-
this.context.call_index = call_index;
|
|
663
|
-
|
|
664
656
|
// I thought we were passing the model as this (...) ? actually
|
|
665
657
|
// now we bind functions that need this, so maybe we should pass
|
|
666
658
|
// null here.
|
|
@@ -988,7 +980,7 @@ export class ExpressionCalculator {
|
|
|
988
980
|
return (expr: UnitGroup) => this.CalculateExpression(expr.elements[0] as ExtendedExpressionUnit);
|
|
989
981
|
}
|
|
990
982
|
|
|
991
|
-
protected CalculateExpression(expr: ExtendedExpressionUnit, return_reference = false): UnionValue
|
|
983
|
+
protected CalculateExpression(expr: ExtendedExpressionUnit, return_reference = false): UnionValue {
|
|
992
984
|
|
|
993
985
|
// user data is a generated function for the expression, at least
|
|
994
986
|
// for the simple ones (atm). see BinaryExpression for more. the
|
|
@@ -996,8 +988,8 @@ export class ExpressionCalculator {
|
|
|
996
988
|
|
|
997
989
|
// may be over-optimizing here.
|
|
998
990
|
|
|
999
|
-
if (expr.
|
|
1000
|
-
return expr.
|
|
991
|
+
if ((expr as AttachCachedFunction<ExpressionUnit>).fn) {
|
|
992
|
+
return (expr as AttachCachedFunction<ExpressionUnit>).fn(expr);
|
|
1001
993
|
}
|
|
1002
994
|
|
|
1003
995
|
switch (expr.type){
|
|
@@ -1005,52 +997,52 @@ export class ExpressionCalculator {
|
|
|
1005
997
|
{
|
|
1006
998
|
const macro = this.data_model.macro_functions.get(expr.name.toUpperCase());
|
|
1007
999
|
if (macro) {
|
|
1008
|
-
return (expr.
|
|
1000
|
+
return (expr.fn = this.CallMacro(expr, macro))(expr);
|
|
1009
1001
|
}
|
|
1010
|
-
return (expr.
|
|
1002
|
+
return (expr.fn = this.CallExpression(expr, return_reference))(expr);
|
|
1011
1003
|
}
|
|
1012
1004
|
|
|
1013
1005
|
case 'address':
|
|
1014
|
-
return (expr.
|
|
1006
|
+
return (expr.fn = this.CellFunction2(expr))(); // check
|
|
1015
1007
|
|
|
1016
1008
|
case 'range':
|
|
1017
|
-
return (expr.
|
|
1009
|
+
return (expr.fn = (x: UnitRange) => this.CellFunction4(x.start, x.end))(expr); // check
|
|
1018
1010
|
|
|
1019
1011
|
case 'binary':
|
|
1020
|
-
return (expr.
|
|
1012
|
+
return (expr.fn = this.BinaryExpression(expr))(expr); // check
|
|
1021
1013
|
|
|
1022
1014
|
case 'unary':
|
|
1023
|
-
return (expr.
|
|
1015
|
+
return (expr.fn = this.UnaryExpression(expr))(expr); // check
|
|
1024
1016
|
|
|
1025
1017
|
case 'identifier':
|
|
1026
|
-
return (expr.
|
|
1018
|
+
return (expr.fn = this.Identifier(expr))(); // check
|
|
1027
1019
|
|
|
1028
1020
|
case 'missing':
|
|
1029
|
-
return (expr.
|
|
1021
|
+
return (expr.fn = () => { return { value: undefined, type: ValueType.undefined } as UndefinedUnion })(); // check
|
|
1030
1022
|
|
|
1031
1023
|
case 'dimensioned':
|
|
1032
|
-
return (expr.
|
|
1024
|
+
return (expr.fn = this.ResolveDimensionedQuantity())(expr);
|
|
1033
1025
|
|
|
1034
1026
|
case 'literal':
|
|
1035
1027
|
{
|
|
1036
1028
|
const literal = { value: expr.value, type: GetValueType(expr.value) } as UnionValue;
|
|
1037
|
-
return (expr.
|
|
1029
|
+
return (expr.fn = () => literal)(); // check
|
|
1038
1030
|
}
|
|
1039
1031
|
case 'group':
|
|
1040
|
-
return (expr.
|
|
1032
|
+
return (expr.fn = this.GroupExpression(expr))(expr); // check
|
|
1041
1033
|
|
|
1042
1034
|
case 'complex':
|
|
1043
1035
|
{
|
|
1044
1036
|
const literal = {value: {real: expr.real, imaginary: expr.imaginary}, type: ValueType.complex } as ComplexUnion;
|
|
1045
|
-
return (expr.
|
|
1037
|
+
return (expr.fn = () => literal)(); // check
|
|
1046
1038
|
}
|
|
1047
1039
|
|
|
1048
1040
|
case 'structured-reference':
|
|
1049
|
-
return (expr.
|
|
1041
|
+
return (expr.fn = this.ResolveStructuredReference(expr))();
|
|
1050
1042
|
|
|
1051
1043
|
case 'array':
|
|
1052
1044
|
{
|
|
1053
|
-
return (expr.
|
|
1045
|
+
return (expr.fn = () => {
|
|
1054
1046
|
return {
|
|
1055
1047
|
type: ValueType.array,
|
|
1056
1048
|
value: expr.values.map((row) => (Array.isArray(row) ? row : [row]).map((value) => {
|
|
@@ -1936,8 +1936,8 @@ for (const name of Object.getOwnPropertyNames(Math)) {
|
|
|
1936
1936
|
case 'function':
|
|
1937
1937
|
// console.info("MATH FUNC", name);
|
|
1938
1938
|
BaseFunctionLibrary[name] = {
|
|
1939
|
-
//
|
|
1940
|
-
fn: (...args:
|
|
1939
|
+
// description: 'Math function',
|
|
1940
|
+
fn: (...args: unknown[]) => {
|
|
1941
1941
|
return Box(value(...args));
|
|
1942
1942
|
},
|
|
1943
1943
|
category: ['Math Functions'],
|
|
@@ -343,7 +343,37 @@ export const StatisticsFunctionLibrary: FunctionMap = {
|
|
|
343
343
|
return { type: ValueType.number, value: (flat[lo-1] + flat[hi-1]) / 2 };
|
|
344
344
|
|
|
345
345
|
},
|
|
346
|
-
}
|
|
346
|
+
},
|
|
347
|
+
|
|
348
|
+
Rank: {
|
|
349
|
+
arguments: [
|
|
350
|
+
{ name: 'Value', },
|
|
351
|
+
{ name: 'Source', },
|
|
352
|
+
{ name: 'Order', },
|
|
353
|
+
],
|
|
354
|
+
fn: (value: number, source: CellValue[], order = 0) => {
|
|
355
|
+
|
|
356
|
+
if (typeof value !== 'number') {
|
|
357
|
+
return ArgumentError();
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const numbers = Utils.FlattenNumbers(source);
|
|
361
|
+
numbers.sort(order ? (a, b) => a - b : (a, b) => b - a);
|
|
362
|
+
|
|
363
|
+
for (let i = 0; i < numbers.length; i++) {
|
|
364
|
+
if (numbers[i] === value) {
|
|
365
|
+
return {
|
|
366
|
+
type: ValueType.number,
|
|
367
|
+
value: i + 1,
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
return ValueError();
|
|
373
|
+
|
|
374
|
+
},
|
|
375
|
+
},
|
|
376
|
+
|
|
347
377
|
|
|
348
378
|
};
|
|
349
379
|
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
|
|
2
|
+
import type { IArea, CellValue } from 'treb-base-types';
|
|
3
|
+
|
|
4
|
+
/** @internal */
|
|
5
|
+
export interface DataValidationRange {
|
|
6
|
+
type: 'range';
|
|
7
|
+
area: IArea;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** @internal */
|
|
11
|
+
export interface DataValidationList {
|
|
12
|
+
type: 'list';
|
|
13
|
+
list: CellValue[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** @internal */
|
|
17
|
+
export interface DataValidationDate {
|
|
18
|
+
type: 'date';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** @internal */
|
|
22
|
+
export interface DataValidationNumber {
|
|
23
|
+
type: 'number';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** @internal */
|
|
27
|
+
export interface DataValidationBoolean {
|
|
28
|
+
type: 'boolean';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** @internal */
|
|
32
|
+
export type DataValidation
|
|
33
|
+
= (DataValidationList
|
|
34
|
+
| DataValidationRange
|
|
35
|
+
| DataValidationNumber
|
|
36
|
+
| DataValidationDate
|
|
37
|
+
| DataValidationBoolean) & {
|
|
38
|
+
|
|
39
|
+
error?: boolean;
|
|
40
|
+
// target: Area[]; // can be multple so we'll default to array
|
|
41
|
+
target: IArea[];
|
|
42
|
+
|
|
43
|
+
}
|
|
44
|
+
|
|
@@ -73,7 +73,7 @@ export class DataModel {
|
|
|
73
73
|
public readonly sheets = new SheetCollection();
|
|
74
74
|
|
|
75
75
|
/** new composite collection (TODO: add macro functions too?) */
|
|
76
|
-
public readonly named = new NamedRangeManager();
|
|
76
|
+
public readonly named = new NamedRangeManager(this.parser);
|
|
77
77
|
|
|
78
78
|
/** macro functions are functions written in spreadsheet language */
|
|
79
79
|
public readonly macro_functions: Map<string, MacroFunction> = new Map();
|
|
@@ -152,15 +152,19 @@ export class DataModel {
|
|
|
152
152
|
[ parse_result.expression, parse_result.expression ];
|
|
153
153
|
|
|
154
154
|
if (start.sheet) {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
if (area.start.sheet_id) {
|
|
158
|
-
this.named.SetNamedRange(named.name, area, scope);
|
|
155
|
+
if (/^\[\d+\]/.test(start.sheet)) {
|
|
156
|
+
console.warn('named range refers to an external file');
|
|
159
157
|
}
|
|
160
158
|
else {
|
|
161
|
-
|
|
159
|
+
const area = new Area({...start, sheet_id: this.sheets.ID(start.sheet), }, end);
|
|
160
|
+
|
|
161
|
+
if (area.start.sheet_id) {
|
|
162
|
+
this.named.SetNamedRange(named.name, area, scope);
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
console.warn("missing sheet ID?", start);
|
|
166
|
+
}
|
|
162
167
|
}
|
|
163
|
-
|
|
164
168
|
}
|
|
165
169
|
else {
|
|
166
170
|
console.warn("missing sheet name?", start);
|
|
@@ -408,7 +412,7 @@ export class DataModel {
|
|
|
408
412
|
(QuotedSheetNameRegex.test(sheet.name) ? `'${sheet.name}'` : sheet.name) : '';
|
|
409
413
|
|
|
410
414
|
return name + (name ? '!' : '') + (parts[0] === parts[1] ? parts[0] : parts.join(':'));
|
|
411
|
-
|
|
415
|
+
|
|
412
416
|
}
|
|
413
417
|
|
|
414
418
|
// --- resolution api, moved from calculator ---------------------------------
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
|
|
22
22
|
import { Area } from 'treb-base-types';
|
|
23
23
|
import type { IArea } from 'treb-base-types';
|
|
24
|
-
import type { ExpressionUnit } from 'treb-parser';
|
|
24
|
+
import type { ExpressionUnit, Parser } from 'treb-parser';
|
|
25
25
|
|
|
26
26
|
interface NamedExpression {
|
|
27
27
|
type: 'expression';
|
|
@@ -82,6 +82,8 @@ export class NamedRangeManager {
|
|
|
82
82
|
return this.named.values();
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
constructor(public parser: Parser) {}
|
|
86
|
+
|
|
85
87
|
/** shorthand for setting named expression */
|
|
86
88
|
public SetNamedExpression(name: string, expression: ExpressionUnit, scope?: number) {
|
|
87
89
|
return this.SetName({
|
|
@@ -206,30 +208,53 @@ export class NamedRangeManager {
|
|
|
206
208
|
* named range rules:
|
|
207
209
|
*
|
|
208
210
|
* - legal characters are alphanumeric, underscore and dot.
|
|
211
|
+
*
|
|
209
212
|
* - must start with letter or underscore (not a number or dot).
|
|
213
|
+
* UPDATE: also possibly a backslash? not sure if that's escaping or not.
|
|
214
|
+
*
|
|
210
215
|
* - cannot look like a spreadsheet address, which is 1-3 letters followed by numbers.
|
|
211
|
-
*
|
|
216
|
+
* UPDATE: actually this is legal if the number is "0". that's the only
|
|
217
|
+
* number. I don't think our parser will process this correctly, so until
|
|
218
|
+
* then keep it illegal.
|
|
219
|
+
*
|
|
212
220
|
* - apparently questuon marks are legal, but not in first position. atm
|
|
213
221
|
* our parser will reject.
|
|
214
222
|
*
|
|
223
|
+
* - FIXME: we should block R1C1-looking notation as well
|
|
224
|
+
*
|
|
215
225
|
* returns a normalized name (just caps, atm)
|
|
216
226
|
*/
|
|
217
227
|
public ValidateNamed(name: string): string|false {
|
|
218
|
-
|
|
228
|
+
|
|
229
|
+
// normalize
|
|
230
|
+
name = name.trim().toUpperCase();
|
|
219
231
|
|
|
220
232
|
// can't be empty
|
|
221
233
|
if (!name.length) return false;
|
|
222
234
|
|
|
223
|
-
// can
|
|
224
|
-
if (
|
|
225
|
-
|
|
226
|
-
// can only contain legal characters
|
|
227
|
-
if (/[^A-Za-z\d_.?]/.test(name)) return false;
|
|
235
|
+
// can only contain legal characters [FIXME: backslash is legal?]
|
|
236
|
+
if (/[^A-Z\d_.?]/.test(name)) return false;
|
|
228
237
|
|
|
229
238
|
// must start with ascii letter or underscore
|
|
230
|
-
if (/^[^A-
|
|
239
|
+
if (/^[^A-Z_]/.test(name)) return false;
|
|
240
|
+
|
|
241
|
+
// these are not legal for some reason -- they look like R1C1?
|
|
242
|
+
if (name === 'R' || name === 'C') {
|
|
243
|
+
return false;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// FIXME: should we just use a parser? we're adding more and more regexes.
|
|
247
|
+
|
|
248
|
+
// can't look like a spreadsheet address, unless the row is === 0
|
|
249
|
+
if (/^[A-Z]{1,3}[1-9]\d*$/.test(name)) return false;
|
|
250
|
+
|
|
251
|
+
// can't look like R1C1 either (we already checked for R and C)
|
|
252
|
+
if (/^R[[\]\d]+C/.test(name)) {
|
|
253
|
+
return false;
|
|
254
|
+
}
|
|
231
255
|
|
|
232
|
-
return name
|
|
256
|
+
return name;
|
|
257
|
+
|
|
233
258
|
}
|
|
234
259
|
|
|
235
260
|
/**
|
|
@@ -43,6 +43,7 @@ import type { GridSelection } from './sheet_selection';
|
|
|
43
43
|
import { CreateSelection } from './sheet_selection';
|
|
44
44
|
import { Annotation } from './annotation';
|
|
45
45
|
import type { ConditionalFormatList } from './conditional_format';
|
|
46
|
+
import type { DataValidation } from './data-validation';
|
|
46
47
|
|
|
47
48
|
// --- constants --------------------------------------------------------------
|
|
48
49
|
|
|
@@ -163,6 +164,11 @@ export class Sheet {
|
|
|
163
164
|
*/
|
|
164
165
|
public conditional_formats: ConditionalFormatList = [];
|
|
165
166
|
|
|
167
|
+
/**
|
|
168
|
+
* @internal
|
|
169
|
+
*/
|
|
170
|
+
public data_validation: DataValidation[] = [];
|
|
171
|
+
|
|
166
172
|
/**
|
|
167
173
|
* @internal
|
|
168
174
|
*
|
|
@@ -255,6 +261,7 @@ export class Sheet {
|
|
|
255
261
|
*/
|
|
256
262
|
private conditional_format_checklist: IArea[] = [];
|
|
257
263
|
|
|
264
|
+
|
|
258
265
|
// --- accessors ------------------------------------------------------------
|
|
259
266
|
|
|
260
267
|
// public get column_header_count() { return this.column_header_count_; }
|
|
@@ -383,6 +390,11 @@ export class Sheet {
|
|
|
383
390
|
sheet.conditional_formats = source.conditional_formats;
|
|
384
391
|
}
|
|
385
392
|
|
|
393
|
+
sheet.data_validation = (source.data_validations || []).map(validation => ({
|
|
394
|
+
...validation,
|
|
395
|
+
target: (validation.target||[]).map(target => new Area(target.start, target.end)),
|
|
396
|
+
}));
|
|
397
|
+
|
|
386
398
|
// persist ID, name
|
|
387
399
|
|
|
388
400
|
if (source.id) {
|
|
@@ -740,6 +752,45 @@ export class Sheet {
|
|
|
740
752
|
|
|
741
753
|
}
|
|
742
754
|
|
|
755
|
+
/** add a data validation. */
|
|
756
|
+
public AddValidation(validation: DataValidation) {
|
|
757
|
+
this.data_validation.push({
|
|
758
|
+
...validation,
|
|
759
|
+
target: (validation.target||[]).map(target => new Area(target.start, target.end)), // ensure class instance
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
/**
|
|
764
|
+
* remove validations from area. must be an exact match (FIXME).
|
|
765
|
+
* if there are multiple areas, only remove the matching area.
|
|
766
|
+
*/
|
|
767
|
+
public RemoveValidations(area: IArea) {
|
|
768
|
+
|
|
769
|
+
const check = new Area(area.start, area.end);
|
|
770
|
+
this.data_validation = this.data_validation.filter(validation => {
|
|
771
|
+
validation.target = validation.target.filter(compare => !check.Equals2(compare));
|
|
772
|
+
return validation.target.length > 0;
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
/** return data validation(s) that apply to a given address */
|
|
777
|
+
public GetValidation(address: ICellAddress) {
|
|
778
|
+
|
|
779
|
+
// switch to imperative
|
|
780
|
+
|
|
781
|
+
const list: DataValidation[] = [];
|
|
782
|
+
for (const entry of this.data_validation) {
|
|
783
|
+
for (const area of entry.target) {
|
|
784
|
+
if ((area as Area).Contains(address)) {
|
|
785
|
+
list.push(entry);
|
|
786
|
+
break;
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
return list;
|
|
792
|
+
|
|
793
|
+
}
|
|
743
794
|
|
|
744
795
|
public Activate(DOM: DOMContext) {
|
|
745
796
|
|
|
@@ -2665,6 +2716,11 @@ export class Sheet {
|
|
|
2665
2716
|
JSON.parse(JSON.stringify(this.conditional_formats.map(format => ({...format, internal: undefined })))) :
|
|
2666
2717
|
undefined;
|
|
2667
2718
|
|
|
2719
|
+
// yes, here. we should have a serialized type so we know to convert. TODO
|
|
2720
|
+
|
|
2721
|
+
const data_validations = this.data_validation.length ? JSON.parse(JSON.stringify(this.data_validation)) : undefined;
|
|
2722
|
+
|
|
2723
|
+
|
|
2668
2724
|
const result: SerializedSheet = {
|
|
2669
2725
|
|
|
2670
2726
|
// not used atm, but in the event we need to gate
|
|
@@ -2688,6 +2744,7 @@ export class Sheet {
|
|
|
2688
2744
|
column_style,
|
|
2689
2745
|
|
|
2690
2746
|
conditional_formats,
|
|
2747
|
+
data_validations,
|
|
2691
2748
|
|
|
2692
2749
|
row_pattern: row_pattern.length ? row_pattern : undefined,
|
|
2693
2750
|
|
|
@@ -2886,6 +2943,10 @@ export class Sheet {
|
|
|
2886
2943
|
this.conditional_formats.push(format);
|
|
2887
2944
|
}
|
|
2888
2945
|
|
|
2946
|
+
for (const validation of data.data_validations || []) {
|
|
2947
|
+
this.AddValidation(validation);
|
|
2948
|
+
}
|
|
2949
|
+
|
|
2889
2950
|
if (data.hidden) {
|
|
2890
2951
|
this.visible = false;
|
|
2891
2952
|
}
|
|
@@ -23,6 +23,7 @@ import type { IArea, SerializedCellData, CellStyle } from 'treb-base-types';
|
|
|
23
23
|
import type { AnnotationData } from './annotation';
|
|
24
24
|
import type { GridSelection, SerializedGridSelection } from './sheet_selection';
|
|
25
25
|
import type { ConditionalFormatList } from './conditional_format';
|
|
26
|
+
import type { DataValidation } from './data-validation';
|
|
26
27
|
|
|
27
28
|
export interface UpdateHints {
|
|
28
29
|
data?: boolean;
|
|
@@ -72,6 +73,9 @@ export interface SerializedSheet {
|
|
|
72
73
|
/** @internal */
|
|
73
74
|
conditional_formats?: ConditionalFormatList;
|
|
74
75
|
|
|
76
|
+
/** @internal */
|
|
77
|
+
data_validations?: DataValidation[];
|
|
78
|
+
|
|
75
79
|
/**
|
|
76
80
|
* @deprecated use `styles` instead
|
|
77
81
|
*/
|