@trebco/treb 29.5.4 → 29.7.5

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.
@@ -58,6 +58,7 @@ type ExpressionWithCachedFunction<T extends ExpressionUnit> = T extends { type:
58
58
  export type ExtendedExpressionUnit = ExpressionWithCachedFunction<ExpressionUnit>;
59
59
 
60
60
  // FIXME: move
61
+ // FIXME: this is sloppy
61
62
  export const UnionIsExpressionUnit = (test: UnionValue /*UnionOrArray*/): test is { type: ValueType.object, value: ExpressionUnit } => {
62
63
  return !Array.isArray(test)
63
64
  && test.type === ValueType.object
@@ -65,15 +66,9 @@ export const UnionIsExpressionUnit = (test: UnionValue /*UnionOrArray*/): test i
65
66
  };
66
67
 
67
68
  // FIXME: move
69
+ // FIXME: this is sloppy
68
70
  export const UnionIsMetadata = (test: UnionValue /*UnionOrArray*/): test is { type: ValueType.object, value: ReferenceMetadata } => {
69
-
70
71
  return test.type === ValueType.object && test.key === 'metadata';
71
-
72
- /*
73
- return !Array.isArray(test)
74
- && test.type === ValueType.object
75
- && ((test.value as ReferenceMetadata).type === 'metadata');
76
- */
77
72
  };
78
73
 
79
74
  // FIXME: move
@@ -89,7 +84,6 @@ export interface ReferenceMetadata {
89
84
 
90
85
  export interface CalculationContext {
91
86
  address: ICellAddress;
92
- // model?: DataModel;
93
87
  volatile: boolean;
94
88
  }
95
89
 
@@ -100,10 +94,6 @@ export class ExpressionCalculator {
100
94
  volatile: false,
101
95
  };
102
96
 
103
- //
104
- // protected data_model!: DataModel; // can we set in ctor? I think this is a legacy hack
105
-
106
-
107
97
  // --- public API -----------------------------------------------------------
108
98
 
109
99
  constructor(
@@ -111,18 +101,6 @@ export class ExpressionCalculator {
111
101
  protected readonly library: FunctionLibrary,
112
102
  protected readonly parser: Parser) {}
113
103
 
114
- /*
115
- public SetModel(model: DataModel): void {
116
-
117
- // is this kept around for some side-effects or something? does
118
- // the model ever change?
119
-
120
- this.data_model = model;
121
- this.context.model = model;
122
-
123
- }
124
- */
125
-
126
104
  /**
127
105
  * there's a case where we are calling this from within a function
128
106
  * (which is weird, but hey) and to do that we need to preserve flags.
@@ -131,10 +109,8 @@ export class ExpressionCalculator {
131
109
  value: UnionValue /*UnionOrArray*/, volatile: boolean }{
132
110
 
133
111
  if (!preserve_flags) {
134
-
135
112
  this.context.address = addr;
136
113
  this.context.volatile = false;
137
-
138
114
  }
139
115
 
140
116
  return {
@@ -154,8 +130,6 @@ export class ExpressionCalculator {
154
130
  if (!expr.sheet_id) {
155
131
  if (expr.sheet) {
156
132
  expr.sheet_id = this.data_model.sheets.ID(expr.sheet) || 0;
157
-
158
- // expr.sheet_id = this.sheet_name_map[expr.sheet.toLowerCase()];
159
133
  }
160
134
  else {
161
135
  return () => ReferenceError();
@@ -178,7 +152,6 @@ export class ExpressionCalculator {
178
152
 
179
153
  if (!cell) {
180
154
  return () => {
181
- // return { type: ValueType.undefined, value: undefined }
182
155
  return { type: ValueType.number, value: 0 };
183
156
  };
184
157
  }
@@ -199,9 +172,7 @@ export class ExpressionCalculator {
199
172
  // throw new Error('missing sheet id in CellFunction4');
200
173
  }
201
174
 
202
- //const cells = this.cells_map[start.sheet_id];
203
175
  const cells = this.data_model.sheets.Find(start.sheet_id)?.cells;
204
-
205
176
  return cells?.GetRange4(start, end, true) || ReferenceError();
206
177
 
207
178
  }
@@ -241,7 +212,6 @@ export class ExpressionCalculator {
241
212
 
242
213
  case 'identifier':
243
214
  {
244
- // const named_range = this.named_range_map[arg.name.toUpperCase()];
245
215
  const named_range = this.data_model.GetName(arg.name, this.context.address.sheet_id || 0);
246
216
  if (named_range?.type === 'range') {
247
217
  if (named_range.area.count === 1) {
@@ -290,29 +260,18 @@ export class ExpressionCalculator {
290
260
 
291
261
  // don't we have a map? [...] only for names?
292
262
 
293
- let sheet: Sheet|undefined; // = this.data_model.active_sheet;
294
- if (address.sheet_id) { // && address.sheet_id !== sheet.id) {
263
+ let sheet: Sheet|undefined;
264
+ if (address.sheet_id) {
295
265
  sheet = this.data_model.sheets.Find(address.sheet_id);
296
-
297
- /*
298
- for (const test of this.data_model.sheets) {
299
- if (test.id === address.sheet_id) {
300
- sheet = test;
301
- break;
302
- }
303
- }
304
- */
305
266
  }
306
267
 
307
268
  if (!sheet) {
308
- // throw new Error('missing sheet [ac8]');
309
269
  console.error('missing sheet [ac8]');
310
270
  return ReferenceError();
311
271
  }
312
272
 
313
273
  const cell_data = sheet.CellData(address);
314
- const value = // (cell_data.type === ValueType.formula) ? cell_data.calculated : cell_data.value;
315
- cell_data.calculated_type ? cell_data.calculated : cell_data.value;
274
+ const value = cell_data.calculated_type ? cell_data.calculated : cell_data.value;
316
275
 
317
276
  const metadata: ReferenceMetadata = {
318
277
  type: 'metadata',
@@ -345,22 +304,12 @@ export class ExpressionCalculator {
345
304
  return ReferenceError();
346
305
  }
347
306
 
348
- let sheet: Sheet|undefined; // = this.data_model.active_sheet;
349
- if (range.start.sheet_id) { // && range.start.sheet_id !== sheet.id) {
307
+ let sheet: Sheet|undefined;
308
+ if (range.start.sheet_id) {
350
309
  sheet = this.data_model.sheets.Find(range.start.sheet_id);
351
- /*
352
- for (const test of this.data_model.sheets) {
353
- if (test.id === range.start.sheet_id) {
354
- sheet = test;
355
- break;
356
- }
357
- }
358
- */
359
310
  }
360
311
 
361
312
  if (!sheet) {
362
- // console.info({range, context: JSON.stringify(this.context.address)});
363
- // console.info({arg});
364
313
  throw new Error('missing sheet [ac9]');
365
314
  }
366
315
 
@@ -372,8 +321,7 @@ export class ExpressionCalculator {
372
321
  const cell_data = sheet.CellData({row, column});
373
322
  address = {...range.start, row, column};
374
323
 
375
- const value = // (cell_data.type === ValueType.formula) ? cell_data.calculated : cell_data.value;
376
- cell_data.calculated_type ? cell_data.calculated : cell_data.value;
324
+ const value = cell_data.calculated_type ? cell_data.calculated : cell_data.value;
377
325
 
378
326
  const metadata = {
379
327
  type: 'metadata',
@@ -464,35 +412,7 @@ export class ExpressionCalculator {
464
412
  }
465
413
 
466
414
  /**
467
- * excutes a function call
468
- *
469
- * the return type of functions has never been locked down, and as a result
470
- * there are a couple of things we need to handle.
471
- *
472
- * return type can be any value, essentially, or array, error object, or
473
- * (in the case of some of the reference/lookup functions) an address or
474
- * range expression. array must be 2d, I think? not sure that that is true.
475
- *
476
- * this wrapper function returns a function which returns one of those
477
- * things, i.e. it returns (expr) => return type
478
- *
479
- * it will only return address/range if the parameter flag is set, so we
480
- * could in theory lock it down a bit with overloads.
481
- *
482
- * ---
483
- *
484
- * UPDATE: that's no longer the case. we require that functions return
485
- * a UnionValue type (union), which can itself contain an array.
486
- *
487
- * ---
488
- *
489
- * FIXME: there is far too much duplication between this and the MC version
490
- * (in simulation-expression-calculator). we need to find a way to consolidate
491
- * these.
492
- *
493
- * I think the problem is that we don't want a lot of switches, but the cost
494
- * is an almost complete duplicate of this function in the subclass.
495
- *
415
+ * excute a function call
496
416
  */
497
417
  protected CallExpression(outer: UnitCall, return_reference = false): (expr: UnitCall) => UnionValue /*UnionOrArray*/ {
498
418
 
@@ -513,45 +433,18 @@ export class ExpressionCalculator {
513
433
 
514
434
  return (expr: UnitCall) => {
515
435
 
516
- // yeah so this is clear. just checking volatile.
517
-
518
- // FIXME: should this be set later, at the same time as the
519
- // calculation index? I think it should, since we may recurse.
520
-
521
- // BEFORE YOU DO THAT, track down all references that read this field
522
-
523
- // from what I can tell, the only place this is read is after the
524
- // external (outer) Calculate() call. so we should move this assignment,
525
- // and we should also be able to get it to fail:
526
- //
527
- // RandBetween() should be volatile, but if we have a nonvolatile function
528
- // as an argument that should unset it, and remove the volatile flag.
529
- // Check?
530
-
531
- // actually this works, because it only sets the flag (does not unset).
532
- // volatile applies to the _cell_, not just the function -- so as long
533
- // as the outer function sets the flag, it's not material if an inner
534
- // function is nonvolatile. similarly an inner volatile function will
535
- // make the outer function volatile.
536
-
537
- // this does mean that the nonvolatile function will be treated differently
538
- // if it's an argument to a volatile function, but I think that's reasonable
539
- // behavior; also it's symmetric with the opposite case (inner volatile.)
540
-
541
- // so leave this as-is, or you can move it -- should be immaterial
542
-
436
+ // set context volatile if this function is volatile. it will bubble
437
+ // through nested function calls, so the entire cell will be volatile
438
+ // if there's a volatile function in there somewhere
439
+
543
440
  this.context.volatile = this.context.volatile || (!!func.volatile);
544
441
 
545
- // NOTE: the argument logic is (possibly) calculating unecessary operations,
546
- // if there's a conditional (like an IF function). although that is the
547
- // exception rather than the rule...
548
-
549
- // ok we can handle IF functions, at the expense of some tests...
550
- // is it worth it?
551
-
442
+ // we recurse calculation, but in the specific case of IF functions
443
+ // we can short-circuit and skip the unused code path. doesn't apply
444
+ // anywhere else atm
445
+
552
446
  const if_function = outer.name.toLowerCase() === 'if';
553
447
  let skip_argument_index = -1;
554
-
555
448
  let argument_error: UnionValue|undefined;
556
449
 
557
450
  const argument_descriptors = func.arguments || []; // map
@@ -593,16 +486,12 @@ export class ExpressionCalculator {
593
486
  } : this.parser.Render(arg).replace(/\$/g, '');
594
487
  }
595
488
  else if (descriptor.metadata) {
596
-
597
489
  return this.GetMetadata(arg, () => { return {}}); // type is UnionOrArray
598
-
599
490
  }
600
491
  else {
601
492
 
602
493
  const result = this.CalculateExpression(arg as ExtendedExpressionUnit);
603
494
 
604
- // if (!Array.isArray(result) && result.type === ValueType.error) {
605
-
606
495
  if (result.type === ValueType.error) { // array check is implicit since array is a type
607
496
  if (descriptor.allow_error) {
608
497
  return result; // always boxed
@@ -613,11 +502,9 @@ export class ExpressionCalculator {
613
502
 
614
503
  // can't shortcut if you have an array (or we need to test all the values)
615
504
 
616
- //if (if_function && arg_index === 0 && !Array.isArray(result)) {
617
- if (if_function && arg_index === 0 && result.type !== ValueType.array){ // !Array.isArray(result)) {
618
- let result_truthy = false;
505
+ if (if_function && arg_index === 0 && result.type !== ValueType.array){
619
506
 
620
- // if (Array.isArray(result)) { result_truthy = true; }
507
+ let result_truthy = false;
621
508
 
622
509
  if (result.type === ValueType.string) {
623
510
  const lowercase = (result.value as string).toLowerCase().trim();
@@ -626,6 +513,7 @@ export class ExpressionCalculator {
626
513
  else {
627
514
  result_truthy = !!result.value;
628
515
  }
516
+
629
517
  skip_argument_index = result_truthy ? 2 : 1;
630
518
  }
631
519
 
@@ -633,11 +521,6 @@ export class ExpressionCalculator {
633
521
  return result;
634
522
  }
635
523
 
636
- /*
637
- if (Array.isArray(result)) {
638
- return result.map(row => row.map(value => value.value));
639
- }
640
- */
641
524
  if (result.type === ValueType.array) {
642
525
  return (result as ArrayUnion).value.map(row => row.map(value => value.value));
643
526
  }
@@ -653,12 +536,6 @@ export class ExpressionCalculator {
653
536
  return argument_error;
654
537
  }
655
538
 
656
- // I thought we were passing the model as this (...) ? actually
657
- // now we bind functions that need this, so maybe we should pass
658
- // null here.
659
-
660
- // return func.fn.apply(null, mapped_args);
661
-
662
539
  if (func.return_type === ReturnType.reference) {
663
540
 
664
541
  const result = func.fn.apply(null, mapped_args);
@@ -757,11 +634,6 @@ export class ExpressionCalculator {
757
634
  value: (operand as ArrayUnion).value.map(column => column.map(value => func(zero, value))),
758
635
  };
759
636
  }
760
- /*
761
- if (Array.isArray(operand)) {
762
- return operand.map(column => column.map(value => func(zero, value)));
763
- }
764
- */
765
637
  return func(zero, operand);
766
638
  };
767
639
 
@@ -929,31 +801,17 @@ export class ExpressionCalculator {
929
801
 
930
802
  return () => {
931
803
 
932
- /*
933
- if (this.bound_name_stack[0]) {
934
- const expr = this.bound_name_stack[0][upper_case];
935
- if (expr) {
936
- console.info("BOUND", upper_case, expr);
937
- return this.CalculateExpression(expr as ExtendedExpressionUnit);
938
- }
939
- }
940
- */
941
-
942
- // const named_range = this.named_range_map[upper_case];
943
- const named_range = this.data_model.GetName(upper_case, this.context.address.sheet_id || 0);
944
-
945
- if (named_range?.type === 'range') {
946
- if (named_range.area.count === 1) {
947
- return this.CellFunction4(named_range.area.start, named_range.area.start);
948
- }
949
- else {
950
- return this.CellFunction4(named_range.area.start, named_range.area.end);
951
- }
952
- }
953
-
954
- const named2 = this.data_model.GetName(identifier, this.context.address.sheet_id || 0);
955
- if (named2 && named2.type === 'expression') {
956
- return this.CalculateExpression(named2.expression as ExtendedExpressionUnit);
804
+ const named = this.data_model.GetName(upper_case, this.context.address.sheet_id || 0);
805
+
806
+ switch (named?.type) {
807
+ case 'range':
808
+ if (named.area.count === 1) {
809
+ return this.CellFunction4(named.area.start, named.area.start);
810
+ }
811
+ return this.CellFunction4(named.area.start, named.area.end);
812
+
813
+ case 'expression':
814
+ return this.CalculateExpression(named.expression as ExtendedExpressionUnit);
957
815
  }
958
816
 
959
817
  // console.info( '** identifier', {identifier, expr, context: this.context});
@@ -40,14 +40,6 @@ export interface FunctionError {
40
40
  error: ErrorType;
41
41
  }
42
42
 
43
- // export const ArgumentError: FunctionError = { error: ErrorType.Argument };
44
- // export const ReferenceError: FunctionError = { error: ErrorType.Reference };
45
- //export const ExpressionError: FunctionError = { error: ErrorType.Expression };
46
- // export const NameError: FunctionError = { error: ErrorType.Name };
47
- // export const ValueError: FunctionError = { error: ErrorType.Value };
48
- // export const DataError: FunctionError = { error: ErrorType.Data };
49
- // export const DivideByZeroError: FunctionError = { error: ErrorType.Div0 };
50
- // export const UnknownError: FunctionError = { error: ErrorType.Unknown };
51
43
  export const NotImplError: FunctionError = { error: ErrorType.NotImpl };
52
44
 
53
45
  export const NAError = (): UnionValue => {
@@ -87,18 +79,21 @@ export const UnknownError = (): UnionValue => {
87
79
  };
88
80
 
89
81
 
90
- /** type guard function */
91
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
92
- export const IsError = (test: any): test is FunctionError => {
93
- return test && typeof test.error && (
94
- test.error === ErrorType.Argument ||
95
- test.error === ErrorType.Reference ||
96
- test.error === ErrorType.Name ||
97
- test.error === ErrorType.Expression ||
98
- test.error === ErrorType.Data ||
99
- test.error === ErrorType.Unknown ||
100
- test.error === ErrorType.NotImpl ||
101
- test.error === ErrorType.Value ||
102
- test.error === ErrorType.Div0
82
+ /**
83
+ * type guard function
84
+ *
85
+ * ...this is maybe too precise?
86
+ */
87
+ export const IsError = (test: unknown): test is FunctionError => {
88
+ return !!test && typeof test === 'object' && !!(test as FunctionError).error && (
89
+ (test as FunctionError).error === ErrorType.Argument ||
90
+ (test as FunctionError).error === ErrorType.Reference ||
91
+ (test as FunctionError).error === ErrorType.Name ||
92
+ (test as FunctionError).error === ErrorType.Expression ||
93
+ (test as FunctionError).error === ErrorType.Data ||
94
+ (test as FunctionError).error === ErrorType.Unknown ||
95
+ (test as FunctionError).error === ErrorType.NotImpl ||
96
+ (test as FunctionError).error === ErrorType.Value ||
97
+ (test as FunctionError).error === ErrorType.Div0
103
98
  );
104
99
  };
@@ -147,7 +147,7 @@ export const MatrixFunctionLibrary: FunctionMap = {
147
147
  },
148
148
 
149
149
  MMult: {
150
- description: 'Returns the dot product of A and B',
150
+ description: 'Returns the dot product A B',
151
151
  arguments: [
152
152
  { name: 'A', boxed: true },
153
153
  { name: 'B', boxed: true },
@@ -23,16 +23,6 @@
23
23
  import type { RangeScale} from 'treb-utils';
24
24
  import { Scale } from 'treb-utils';
25
25
 
26
- /* * calculated human-friendly scale for rendering axes * /
27
- export interface RangeScale {
28
- scale: number;
29
- step: number;
30
- count: number;
31
- min: number;
32
- max: number;
33
- }
34
- */
35
-
36
26
  export class Util {
37
27
 
38
28
  /**
@@ -63,32 +53,9 @@ export class Util {
63
53
  return range * (value - scale.min) / (scale.max - scale.min);
64
54
  }
65
55
 
66
- /* *
67
- * flatten. we support holes in data, which means undefined values
68
- * in arrays, but don't push an empty value at the top level (if
69
- * that makes sense).
70
- *
71
- * @deprecated
72
- * /
73
- public static Flatten(args: any) {
74
- let flat: any[] = [];
75
- if (Array.isArray(args)) {
76
- for (const element of args) {
77
- if (Array.isArray(element)) {
78
- flat = flat.concat(this.Flatten(element));
79
- }
80
- else {
81
- flat.push(element);
82
- }
83
- }
84
- }
85
- else if (typeof args !== 'undefined') {
86
- flat.push(args);
87
- }
88
- return flat;
89
- }
90
- */
91
-
56
+ /**
57
+ * can we replace this with Array.flatMap?
58
+ */
92
59
  public static Flatten<T>(args?: T|T[]|T[][]) {
93
60
  let flat: T[] = [];
94
61
  if (Array.isArray(args)) {
@@ -21,34 +21,13 @@
21
21
 
22
22
  import type { Sheet } from './sheet';
23
23
  import { SheetCollection } from './sheet_collection';
24
+ import { type UnitAddress, type UnitStructuredReference, type UnitRange, Parser, QuotedSheetNameRegex, DecimalMarkType, ArgumentSeparatorType } from 'treb-parser';
24
25
  import type { IArea, ICellAddress, Table, CellStyle } from 'treb-base-types';
25
- import type { SerializedSheet } from './sheet_types';
26
- import { type ExpressionUnit, type UnitAddress, type UnitStructuredReference, type UnitRange, Parser, QuotedSheetNameRegex, DecimalMarkType, ArgumentSeparatorType } from 'treb-parser';
27
26
  import { Area, IsCellAddress, Style } from 'treb-base-types';
28
27
  import type { SerializedNamed } from './named';
29
28
  import { NamedRangeManager } from './named';
29
+ import type { ConnectedElementType, MacroFunction } from './types';
30
30
 
31
- export interface ConnectedElementType {
32
- formula: string;
33
- update?: (instance: ConnectedElementType) => void;
34
- internal?: unknown; // opaque type to prevent circular dependencies
35
- }
36
-
37
- export interface SerializedMacroFunction {
38
- name: string;
39
- function_def: string;
40
- argument_names?: string[];
41
- description?: string;
42
- }
43
-
44
- /**
45
- * we define this as extending the serialized version, rather
46
- * than taking out the parameter, so we can make that def public
47
- * in the API types.
48
- */
49
- export interface MacroFunction extends SerializedMacroFunction {
50
- expression?: ExpressionUnit;
51
- }
52
31
 
53
32
  /**
54
33
  *
@@ -546,43 +525,3 @@ export class DataModel {
546
525
 
547
526
  }
548
527
 
549
- /**
550
- * @internal
551
- */
552
- export interface ViewModel {
553
- active_sheet: Sheet;
554
- view_index: number;
555
- }
556
-
557
- /**
558
- * this type is no longer in use, but we retain it to parse old documents
559
- * that use it.
560
- *
561
- * @deprecated
562
- */
563
- export interface SerializedNamedExpression {
564
- name: string;
565
- expression: string;
566
- }
567
-
568
- export interface SerializedModel {
569
- sheet_data: SerializedSheet[];
570
- active_sheet: number;
571
-
572
- /** @deprecated */
573
- named_ranges?: Record<string, IArea>;
574
-
575
- /** @deprecated */
576
- named_expressions?: SerializedNamedExpression[];
577
-
578
- /**
579
- * new type for consolidated named ranges & expressions. the old
580
- * types are retained for backwards compatibility on import but we won't
581
- * export them anymore.
582
- */
583
- named?: SerializedNamed[];
584
-
585
- macro_functions?: MacroFunction[];
586
- tables?: Table[];
587
- decimal_mark?: ','|'.';
588
- }
@@ -20,16 +20,6 @@
20
20
  */
21
21
 
22
22
  export { DataModel } from './data_model';
23
-
24
- export type {
25
- MacroFunction,
26
- ConnectedElementType,
27
- SerializedNamedExpression,
28
- SerializedModel,
29
- ViewModel,
30
- SerializedMacroFunction,
31
- } from './data_model';
32
-
33
23
  export type { SerializedNamed } from './named';
34
24
 
35
25
  export { Sheet } from './sheet';
@@ -43,3 +33,5 @@ export type { ViewData as AnnotationViewData } from './annotation';
43
33
  export type { AnnotationData, AnnotationType } from './annotation';
44
34
 
45
35
  export * from './data-validation';
36
+ export * from './types';
37
+
@@ -2225,8 +2225,14 @@ export class Sheet {
2225
2225
  return this.CompositeStyleForCell(area, true, false, apply_theme);
2226
2226
  }
2227
2227
 
2228
+ // the contract says this should return an array, not a single value.
2229
+ //
2230
+ // I can fix it, but will anyone break? (...) check the indent buttons
2231
+ // (update: looks OK)
2232
+ //
2233
+
2228
2234
  if (area.start.row === area.end.row && area.start.column === area.end.column) {
2229
- return this.CompositeStyleForCell(area.start, true, false, apply_theme);
2235
+ return [[this.CompositeStyleForCell(area.start, true, false, apply_theme)]];
2230
2236
  }
2231
2237
 
2232
2238
  const result: CellStyle[][] = [];
@@ -2234,8 +2240,6 @@ export class Sheet {
2234
2240
  for (let r = area.start.row; r <= area.end.row; r++) {
2235
2241
  const row: CellStyle[] = [];
2236
2242
  for (let c = area.start.column; c <= area.end.column; c++) {
2237
- // const cell = this.CellData({row: r, column: c});
2238
- // row.push(cell.style || {});
2239
2243
  row.push(this.CompositeStyleForCell({row: r, column: c}, true, false, apply_theme));
2240
2244
  }
2241
2245
  result.push(row);