@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.
@@ -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
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
40
- export type ExtendedExpressionUnit = ExpressionUnit & { user_data: any }; // export for MC overload
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.cells_map = {};
118
- // this.sheet_name_map = {};
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
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
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
- address: {...address},
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
- ...map_result(cell_data, address),
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
- ...map_result(cell_data, address),
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 /*UnionOrArray*/ {
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.user_data) {
1000
- return expr.user_data(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.user_data = this.CallMacro(expr, macro))(expr);
1000
+ return (expr.fn = this.CallMacro(expr, macro))(expr);
1009
1001
  }
1010
- return (expr.user_data = this.CallExpression(expr, return_reference))(expr);
1002
+ return (expr.fn = this.CallExpression(expr, return_reference))(expr);
1011
1003
  }
1012
1004
 
1013
1005
  case 'address':
1014
- return (expr.user_data = this.CellFunction2(expr))(); // check
1006
+ return (expr.fn = this.CellFunction2(expr))(); // check
1015
1007
 
1016
1008
  case 'range':
1017
- return (expr.user_data = (x: UnitRange) => this.CellFunction4(x.start, x.end))(expr); // check
1009
+ return (expr.fn = (x: UnitRange) => this.CellFunction4(x.start, x.end))(expr); // check
1018
1010
 
1019
1011
  case 'binary':
1020
- return (expr.user_data = this.BinaryExpression(expr))(expr); // check
1012
+ return (expr.fn = this.BinaryExpression(expr))(expr); // check
1021
1013
 
1022
1014
  case 'unary':
1023
- return (expr.user_data = this.UnaryExpression(expr))(expr); // check
1015
+ return (expr.fn = this.UnaryExpression(expr))(expr); // check
1024
1016
 
1025
1017
  case 'identifier':
1026
- return (expr.user_data = this.Identifier(expr))(); // check
1018
+ return (expr.fn = this.Identifier(expr))(); // check
1027
1019
 
1028
1020
  case 'missing':
1029
- return (expr.user_data = () => { return { value: undefined, type: ValueType.undefined } as UndefinedUnion })(); // check
1021
+ return (expr.fn = () => { return { value: undefined, type: ValueType.undefined } as UndefinedUnion })(); // check
1030
1022
 
1031
1023
  case 'dimensioned':
1032
- return (expr.user_data = this.ResolveDimensionedQuantity())(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.user_data = () => literal)(); // check
1029
+ return (expr.fn = () => literal)(); // check
1038
1030
  }
1039
1031
  case 'group':
1040
- return (expr.user_data = this.GroupExpression(expr))(expr); // check
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.user_data = () => literal)(); // check
1037
+ return (expr.fn = () => literal)(); // check
1046
1038
  }
1047
1039
 
1048
1040
  case 'structured-reference':
1049
- return (expr.user_data = this.ResolveStructuredReference(expr))();
1041
+ return (expr.fn = this.ResolveStructuredReference(expr))();
1050
1042
 
1051
1043
  case 'array':
1052
1044
  {
1053
- return (expr.user_data = () => {
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
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1940
- fn: (...args: any) => {
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
- const area = new Area({...start, sheet_id: this.sheets.ID(start.sheet), }, end);
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
- console.warn("missing sheet ID?", start);
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 ---------------------------------
@@ -42,4 +42,4 @@ export { Annotation } from './annotation';
42
42
  export type { ViewData as AnnotationViewData } from './annotation';
43
43
  export type { AnnotationData, AnnotationType } from './annotation';
44
44
 
45
-
45
+ export * from './data-validation';
@@ -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
- name = name.trim();
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't look like a spreadsheet address
224
- if (/^[A-Za-z]{1,3}\d+$/.test(name)) return false;
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-Za-z_]/.test(name)) return false;
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.toUpperCase();
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
  */