/*
* This file is part of TREB.
*
* TREB is free software: you can redistribute it and/or modify it under the
* terms of the GNU General Public License as published by the Free Software
* Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* TREB is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
* details.
*
* You should have received a copy of the GNU General Public License along
* with TREB. If not, see .
*
* Copyright 2022-2026 trebco, llc.
* info@treb.app
*
*/
/**
* switched to row-major, seems to have no ill effects
* (not sure if there are benefits yet either)
*/
import type { IArea, ICellAddress} from './area';
import { Area, IsCellAddress } from './area';
// import type { DataValidation } from './cell';
import { Cell } from './cell';
import type { Table } from './table';
import { type SerializedValueType, ValueType, GetValueType, ValueTypeList } from './value-type';
import type { CellValue, UnionValue } from './union';
import type { CellStyle } from './style';
export interface CellSerializationOptions {
preserve_type?: boolean;
convert_address?: boolean;
calculated_value?: boolean;
expand_arrays?: boolean;
subset?: Area;
preserve_empty_strings?: boolean;
decorated_cells?: boolean;
tables?: boolean;
/**
* nest rows in columns, or vice-versa, depending on which is smaller.
*/
nested?: boolean;
/**
* cell style refs to pack into cells
*/
cell_style_refs?: number[][];
/** optionally attach an ID to the cells */
sheet_id?: number;
}
// our cell data is now somewhat complicated, from a type perspective. we
// support the original type, which was just an array of cells with each
// cell having {row, column}.
//
// more recent code compresses this by nesting blocks of rows or columns,
// using the structure (e.g.) { row, cells } where each cell in cells has
// a column.
//
// so type needs to support both flat and nested, where nested can be row-
// dominant or column-dominant.
//
// by the way, did we ever validate that this structure is significantly
// smaller, when compressed? (...)
export interface BaseCellData {
value: CellValue;
style_ref?: number;
calculated?: CellValue;
table?: Table;
area?: IArea;
merge_area?: IArea;
// validation?: DataValidation;
calculated_type?: SerializedValueType; // ValueType;
note?: string;
hyperlink?: string;
type?: SerializedValueType; // ValueType;
sheet_id?: number;
spill?: IArea;
// locked?: boolean;
}
/**
* this type is for serialized data that includes the row and column
* in each cell. this was the original serialized data type, and is
* still supported. current serialization will group data into rows or
* columns, whichever results in a smaller overall serialized representation.
*/
export interface CellDataWithAddress extends BaseCellData {
row: number;
column: number;
}
export interface NestedCellData {
cells: BaseCellData[];
}
/**
* this type is for serialized data that is grouped by row, with each
* cell referencing a column in the spreadsheet.
*/
export interface CellDataWithColumn extends BaseCellData {
column: number;
}
export interface NestedRowData extends NestedCellData {
row: number;
cells: CellDataWithColumn[];
}
/**
* this type is for serialized data that is grouped by column, with each
* cell referencing a row in the spreadsheet.
*/
export interface CellDataWithRow extends BaseCellData {
row: number;
}
export interface NestedColumnData extends NestedCellData {
column: number;
cells: CellDataWithRow[];
}
export type SerializedCellData = CellDataWithAddress[]|NestedRowData[]|NestedColumnData[];
// some type guards for the various data types
/** @internal */
export const IsFlatData = (test: CellDataWithAddress|NestedCellData): test is CellDataWithAddress => {
return !(test as NestedCellData).cells;
}
/** @internal */
export const IsFlatDataArray = (test: CellDataWithAddress[]|NestedCellData[]): test is CellDataWithAddress[] => {
return (!!test[0]) && IsFlatData(test[0]);
};
/** @internal */
export const IsNestedRowArray = (test: NestedRowData[]|NestedColumnData[]): test is NestedRowData[] => {
return (!!test[0]) && ((test[0] as NestedRowData).row !== undefined);
};
// ...
/**
* this is the reverse map, i.e. type => number
* FIXME: why is this getting exported by the API generator?
* FIXME: I get why it's dynamic, but for practical purposes why not just
* create a static map?
*/
const ValueTypeMap =
ValueTypeList.map((key, index) => ({ [key]: index })).reduce((set, value) => ({...set, ...value}), {}) as Record;
/**
* collection of cells, basically a wrapper around an
* array, with some accessor and control methods.
*/
export class Cells {
/** switching to row-major */
public data: Cell[][] = [];
private rows_ = 0;
private columns_ = 0;
get rows(): number { return this.rows_; }
get columns(): number { return this.columns_; }
/**
* the sheet wants to make sure this row exists, probably because it has
* a header. so we will update our dimensions to match. we don't actually
* add data.
*
* this is not serialized. specific headers aren't serialized either, at
* the moment, so it's sort of irrelevant. if we start serializing headers,
* the deserialization routine can call this function to pad out, so we
* don't need to store it here.
*/
public EnsureRow(row: number): void {
this.rows_ = Math.max(row + 1, this.rows_);
}
/** @see EnsureRow */
public EnsureColumn(column: number): void {
this.columns_ = Math.max(column + 1, this.columns_);
}
/**
* this class does none of the validation/correction
* required when inserting rows/columns. that should
* be done by external logic. this method only does
* the mechanical work of inserting rows/columns.
*/
public InsertColumns(before = 0, count = 1): void {
// const pre = JSON.parse(JSON.stringify(this.data[13]));
// NOTE: iterating a sparse array, in chrome at least, only
// hits populated keys. the returned array has the same
// indexes. that is very nice.
this.data = this.data.map(row => {
if (row.length >= before){
const tmp = row.slice(0, before);
let index = before + count;
// this forEach is broken when there are empty values in the row,
// which doesn't happen so much anymore but can (and does) happen
// in some older sheets.
// row.slice(before).forEach((column) => tmp[index++] = column);
// do it with an explicit index loop, should resolve
const after = row.slice(before);
for (let i = 0; i < after.length; i++) {
tmp[index++] = after[i];
}
return tmp;
}
return row;
});
this.columns_ += count;
// wtf is this? some old debug stuff?
// const clone = JSON.parse(JSON.stringify(this.data[13]));
// console.info({pre, clone});
}
public DeleteColumns(index: number, count= 1): void {
// trap! splice returns _removed_ elements so don't use map()
this.data.forEach((row) => row.splice(index, count));
this.columns_ -= count;
}
public DeleteRows(index: number, count = 1): void {
this.data.splice(index, count);
this.rows_ -= count;
}
/**
* this class does none of the validation/correction
* required when inserting rows/columns. that should
* be done by external logic. this method only does
* the mechanical work of inserting rows/columns.
*/
public InsertRows(before = 0, count = 1): void {
const args: [number, number, Cell[]] = [before, 0, []];
for ( let i = 1; i < count; i++) args.push([]);
Array.prototype.splice.apply(this.data, args);
this.rows_ += count;
}
/**
* return or create cell at the given address
*/
public GetCell(address: ICellAddress, create_new: true): Cell;
/**
* return the cell at the given address or undefined if it doesn't exist
*/
public GetCell(address: ICellAddress, create_new?: false): Cell | undefined;
/**
* return the given cell or `undefined`, optionally creating
* new cells as necessary
*
* @param create_new always return a cell
*/
public GetCell(address: ICellAddress, create_new?: boolean): Cell|undefined {
const { row, column } = address;
if (!this.data[row]) {
if (create_new) {
this.data[row] = [];
this.rows_ = Math.max(this.rows_, row + 1);
}
else return undefined;
}
if (!this.data[row][column]) {
if (create_new) {
this.data[row][column] = new Cell();
this.columns_ = Math.max(this.columns_, column + 1);
}
}
return this.data[row][column];
}
/**
* apply function to range or address. skips empty cells (for now...)
* (already have this function, it's called "IterateArea". "Apply" is better.)
* /
public Apply(target: ICellAddress|IArea, func: (cell: Cell) => void): void {
if (IsCellAddress(target)) {
target = new Area(target);
}
const start = target.start;
const end = target.end;
for (let r = start.row; r <= end.row; r++) {
if (this.data[r]) {
const row = this.data[r];
for (let c = start.column; c < end.column; c++) {
if (this.data[r][c]) {
func.call(undefined, row[c]);
}
}
}
}
}
*/
/** returns an existing cell or creates a new cell. */
public EnsureCell(address: ICellAddress): Cell {
const { row, column } = address;
let ref = this.data[row];
if (!ref) {
this.data[row] = ref = [];
this.rows_ = Math.max(this.rows_, row + 1);
}
let cell = ref[column];
if (!cell) {
cell = ref[column] = new Cell();
this.columns_ = Math.max(this.columns_, column + 1);
}
return cell;
}
/**
* with the update, we assume the passed-in data is row-major.
* when reading an older file, transpose.
*/
public FromArray(data: CellValue[][] = [], transpose = false): void {
this.data = [];
let rows = 0;
let columns = 0;
if (transpose){
columns = data.length;
for ( let c = 0; c < columns; c++ ){
const ref = data[c];
rows = Math.max(rows, ref.length);
for ( let r = 0; r < ref.length; r++ ){
if (!this.data[r]) this.data[r] = [];
this.data[r][c] = new Cell(ref[r]);
}
}
}
else {
rows = data.length;
for ( let r = 0; r < rows; r++ ){
const column: Cell[] = [];
const ref = data[r];
columns = Math.max(columns, ref.length);
for ( let c = 0; c < ref.length; c++ ) column[c] = new Cell(ref[c]);
this.data[r] = column;
}
}
this.rows_ = rows;
this.columns_ = columns;
}
public SerializedTypeToValueType(type?: SerializedValueType|ValueType): ValueType|undefined {
if (!type) {
return undefined;
}
if (typeof type === 'number') {
return type as ValueType;
}
return ValueTypeMap[type] || undefined;
}
public ValueTypeToSerializedType(type?: ValueType): SerializedValueType|undefined {
return type ? ValueTypeList[type] : undefined;
}
/* *
* this method is used for importing legacy data validation types. in those
* those we used a numeric enum. we're just dropping that altogether (c.f.
* ValueType, which we're keeping) so we need to translate for backcompat.
* it's ugly, but it gets us to a better place. we can probably drop at some
* point in the future.
*
* export enum ValidationType {
* List = 'list',
* Date = 'date',
* Range = 'range',
* Number = 'number',
* Boolean = 'boolean',
* }
*
* OK, removed
* /
public ImportDataValidation(value: DataValidation): DataValidation|undefined {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const type: DataValidation['type']|number = (value as any).type;
if (typeof type === 'number') {
const types: Array = ['list', 'date', 'range', 'number', 'boolean'];
value.type = types[type];
if (!value.type) {
return undefined;
}
}
return value;
}
*/
/**
* UPDATE: adding optional style refs, for export
*/
public FromJSON(data: SerializedCellData = [], style_refs?: CellStyle[]): void {
this.data = [];
// handle nested data; fix. we can make the simplifying assumption
// that data is either nested, or not, but never both. therefore, we
// just need to check the first element.
if (!IsFlatDataArray(data)) {
const new_data: CellDataWithAddress[] = [];
if (IsNestedRowArray(data)) {
for (const block of data) {
for (const cell of block.cells) {
new_data.push({...cell, row: block.row});
}
}
}
else {
for (const block of data) {
for (const cell of block.cells) {
new_data.push({...cell, column: block.column});
}
}
}
data = new_data;
}
/*
if (data[0] && data[0].cells) {
// console.info('reading nested data');
const new_data: any[] = [];
for (const element of data) {
if (typeof element.row !== 'undefined') {
for (const cell of element.cells) {
new_data.push({row: element.row, ...cell});
}
}
else if (typeof element.column !== 'undefined') {
for (const cell of element.cells) {
new_data.push({column: element.column, ...cell});
}
}
}
data = new_data;
}
*/
const tables: Table[] = [];
for (const obj of data) {
if (!this.data[obj.row]) this.data[obj.row] = [];
const cell = new Cell(obj.value);
if (typeof obj.calculated !== 'undefined') {
// cell.calculated = obj.calculated;
// cell.calculated_type = obj.calculated_type;
cell.SetCalculatedValue(obj.calculated, this.SerializedTypeToValueType(obj.calculated_type));
if (obj.spill) {
cell.spill = new Area(obj.spill.start, obj.spill.end);
}
}
if (style_refs) {
if (typeof obj.style_ref !== 'undefined') {
cell.style = style_refs[obj.style_ref];
}
}
if (typeof obj.note !== 'undefined') {
cell.note = obj.note;
}
if (typeof obj.hyperlink !== 'undefined') {
cell.hyperlink = obj.hyperlink;
}
// stop wrecking arrays
if (this.data[obj.row][obj.column] && this.data[obj.row][obj.column].area) {
cell.area = this.data[obj.row][obj.column].area;
}
this.data[obj.row][obj.column] = cell;
// since we are serializing the array data (when storing calculated
// values), is this getting called every time? I think it might be...
// we're fixing the former, anyway.
if (obj.area){
const area = new Area(obj.area.start, obj.area.end); // isn't there a clone method?
for ( let row = area.start.row; row <= area.end.row; row++){
for ( let column = area.start.column; column <= area.end.column; column++){
if (!this.data[row]) this.data[row] = [];
if (!this.data[row][column]) this.data[row][column] = new Cell();
this.data[row][column].area = area;
}
}
}
// collect tables, then apply them after reading all the cells.
// FIXME: why are we not doing this for merges? would be more
// efficient, no?
if (obj.table) {
tables.push({
...obj.table,
});
/*
for ( let row = table.area.start.row; row <= table.area.end.row; row++){
for ( let column = table.area.start.column; column <= table.area.end.column; column++){
if (!this.data[row]) this.data[row] = [];
if (!this.data[row][column]) this.data[row][column] = new Cell();
this.data[row][column].table = table;
}
}
*/
}
if (obj.merge_area){
const merge_area = new Area(obj.merge_area.start, obj.merge_area.end);
for ( let row = merge_area.start.row; row <= merge_area.end.row; row++){
for ( let column = merge_area.start.column; column <= merge_area.end.column; column++){
if (!this.data[row]) this.data[row] = [];
if (!this.data[row][column]) this.data[row][column] = new Cell();
this.data[row][column].merge_area = merge_area;
}
}
}
/*
if (obj.validation) {
// the old type used a numeric enum. we just dropped that in favor
// of a string enum, so we can export it as a type. but for backwards
// compatibility we still need to think about the numeric enum.
cell.validation = this.ImportDataValidation(obj.validation);
}
*/
}
for (const table of tables) {
for ( let row = table.area.start.row; row <= table.area.end.row; row++){
for ( let column = table.area.start.column; column <= table.area.end.column; column++){
if (!this.data[row]) this.data[row] = [];
if (!this.data[row][column]) this.data[row][column] = new Cell();
this.data[row][column].table = table;
}
}
}
this.rows_ = this.data.length;
this.columns_ = this.data.reduce((max, row) => Math.max(max, row.length), 0);
}
public toJSON(options: CellSerializationOptions = {}) : {
data: SerializedCellData;
rows: number;
columns: number;
} {
let start_column = 0;
let start_row = 0;
let end_row = this.data.length - 1;
let end_column;
if (options.subset){
start_column = options.subset.start.column;
start_row = options.subset.start.row;
end_row = options.subset.end.row;
}
const data: CellDataWithAddress[] = [];
let last_row = -1;
let last_col = -1;
// unifying [FIXME: move into class]
// FIXME: why not use the original, instead of requiring a method
// call, and then re-order? that also makes it easier to pivot
// (order by rows or columns)
// ... (we did that)
const row_keys: {[index: number]: number} = {};
const column_keys: {[index: number]: number} = {};
for ( let row = start_row; row <= end_row; row++ ){
if ( this.data[row]){
const ref = this.data[row];
end_column = ref.length - 1;
if (options.subset) end_column = options.subset.end.column;
for ( let column = start_column; column <= end_column; column++ ){
const cell = ref[column];
// because only the array head will have a value, this test
// will filter out empty cells and non-head array cells
// update: also add merge heads
const merge_head = cell && cell.merge_area
&& cell.merge_area.start.row === row
&& cell.merge_area.start.column === column;
const array_head = cell && cell.area
&& cell.area.start.row === row
&& cell.area.start.column === column;
const table_head = cell && cell.table
&& cell.table.area.start.row === row
&& cell.table.area.start.column === column;
const is_empty = cell ? (cell.type === ValueType.string && !cell.value) : true;
// NOTE: we added the check on calculated && calculated_value,
// so we preserve rendered data for arrays. but that actually writes
// the array data as well, which is unnecessary (?) -- FIXME
//
// actually, check how that's interpreted on load, because it might
// break if we have a value but not the array area (...)
// FIXME: what's up with this? we check style? (...) can't recall
// why we do that, because we should ensure empty cells if there's
// a style (separately).
// NOTE: switching test from "calculated" to "calculated type": this
// should preserve zeros.
if (cell && (!is_empty || options.preserve_empty_strings) &&
(merge_head || cell.type || (cell.calculated_type && options.expand_arrays) ||
(cell.calculated_type && options.calculated_value) ||
(cell.note) ||
// (cell.validation) ||
(options.decorated_cells && cell.style &&
( cell.style.fill || cell.style.border_bottom ||
cell.style.border_top || cell.style.border_left || cell.style.border_right)))){
const obj: CellDataWithAddress = { row, column, value: cell.value };
if (cell.note) {
obj.note = cell.note;
}
if (cell.hyperlink) {
obj.hyperlink = cell.hyperlink;
}
if (options.preserve_type) {
obj.type = this.ValueTypeToSerializedType(cell.type);
}
if (options.sheet_id) obj.sheet_id = options.sheet_id;
if (options.calculated_value &&
typeof cell.calculated !== 'undefined') { // && cell.calculated_type !== ValueType.error) {
obj.calculated = cell.calculated;
if (cell.spill) {
obj.spill = cell.spill.toJSON();
}
// always preserve error type, because we can't infer
if (options.preserve_type || cell.calculated_type === ValueType.error) {
obj.calculated_type = this.ValueTypeToSerializedType(cell.calculated_type);
}
}
if (cell.table && table_head) {
if (options.tables) {
obj.table = JSON.parse(JSON.stringify(cell.table));
}
}
if (cell.area && array_head) {
obj.area = cell.area.toJSON();
}
if (cell.merge_area) {
obj.merge_area = cell.merge_area.toJSON();
}
// if (cell.validation) {
// obj.validation = cell.validation; // safe?
// }
if (options.cell_style_refs &&
options.cell_style_refs[column] &&
options.cell_style_refs[column][row]) {
obj.style_ref = options.cell_style_refs[column][row];
options.cell_style_refs[column][row] = 0; // consume
// console.info(`consume @ ${column}, ${row}: ${obj.style_ref } => ${options.cell_style_refs[column][row]}`);
}
row_keys[row] = row;
column_keys[column] = column;
last_row = Math.max(row, last_row);
last_col = Math.max(column, last_col);
data.push(obj);
}
}
}
}
if (options.nested) {
const row_key_map = Object.keys(row_keys);
const col_key_map = Object.keys(column_keys);
// extra test to make sure it's not empty
if ((row_key_map.length <= col_key_map.length) && row_key_map.length) {
const cells: {[index: number]: Array} = {};
// use rows
const new_data: NestedRowData[] = [];
for (const element of data) {
// the construction here seems to be removing the
// "row" key -- is there a better way to do that?
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const {row, ...remainder} = element;
if (!cells[element.row]) cells[element.row] = [];
cells[element.row].push(remainder);
}
for (const key of row_key_map) {
const row = Number(key);
new_data.push({ row, cells: cells[row] });
}
return { data: new_data, rows: last_row, columns: last_col + 1 };
}
else if (col_key_map.length) {
const cells: {[index: number]: Array} = {};
// use columns
const new_data: NestedColumnData[] = [];
for (const element of data) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const {column, ...remainder} = element;
if (!cells[element.column]) cells[element.column] = [];
cells[element.column].push(remainder);
}
for (const key of col_key_map) {
const column = Number(key);
new_data.push({ column, cells: cells[column] });
}
return { data: new_data, rows: last_row, columns: last_col + 1 };
}
}
return { data, rows: last_row + 1, columns: last_col + 1 };
}
public GetAll(transpose = false){
return this.GetRange({row: 0, column: 0}, {row: this.rows_ - 1, column: this.columns_ - 1}, transpose);
}
/** overload */
public Normalize(from: ICellAddress, to: ICellAddress): { from: ICellAddress, to: ICellAddress };
/** overload */
public Normalize(from: ICellAddress): { from: ICellAddress };
/** overload */
public Normalize(from: ICellAddress, to: K): { from: ICellAddress, to: K };
/** base */
public Normalize(from: ICellAddress, to?: ICellAddress): { from: ICellAddress, to?: ICellAddress } {
from = {
...from,
row: from.row == Infinity ? 0 : from.row,
column: from.column == Infinity ? 0 : from.column,
}
if (to) {
to = {
...to,
row: to.row == Infinity ? this.rows_ - 1 : to.row,
column: to.column == Infinity ? this.columns_ - 1 : to.column,
}
}
return { from, to };
}
/**
* get raw values (i.e. not calculated). anything outside of actual
* range will be undefined OR not populated.
*
* to match GetRange, we return a single value in the case of a single cell,
* or a matrix.
*
* NOTE that I'm not sure this is good behavior. if you're going to
* return a single value for one cell, you should return a vector for
* a single row OR a single column. alternatively, you should always
* return a matrix.
*
* @param from
* @param to
* @param transpose
*/
public RawValue(from: ICellAddress, to: ICellAddress = from): CellValue | CellValue[][] | undefined {
({from, to} = this.Normalize(from, to));
if (from.row === to.row && from.column === to.column) {
if (this.data[from.row] && this.data[from.row][from.column]) {
return this.data[from.row][from.column].value;
}
return undefined;
}
const result: CellValue[][] = [];
// grab rows
const rows = this.data.slice(from.row, to.row + 1);
// now columns
const start = from.column;
const end = to.column + 1;
for (const source of rows) {
const target: CellValue[] = [];
for (let column = start, index = 0; column < end; column++, index++ ) {
const cell = source[column];
target.push(cell ? cell.value : undefined);
}
result.push(target);
}
return result;
}
/** gets range as values */
public GetRange(from: ICellAddress, to?: ICellAddress, transpose = false){
({from, to} = this.Normalize(from, to));
// console.info("getrange", from, to, transpose);
if (!to || from === to || (from.column === to.column && from.row === to.row )){
if (this.data[from.row] && this.data[from.row][from.column]){
return this.data[from.row][from.column].GetValue();
}
return undefined;
}
const value: CellValue[][] = [];
if (transpose){
for ( let c = from.column; c <= to.column; c++ ){
const column: CellValue[] = [];
for ( let r = from.row; r <= to.row; r++ ){
if (this.data[r] && this.data[r][c]) column.push(this.data[r][c].GetValue());
else column.push(undefined);
}
value.push(column);
}
}
else {
for ( let r = from.row; r <= to.row; r++ ){
const row: CellValue[] = [];
for ( let c = from.column; c <= to.column; c++ ){
if (this.data[r] && this.data[r][c]) row.push(this.data[r][c].GetValue());
else row.push(undefined);
}
value.push(row);
}
}
// console.info(value)
return value;
}
/* *
* updated version of GetRange that preserves errors, by calling
* the GetValue2 cell function.
* /
public GetRange2(from: ICellAddress, to?: ICellAddress, transpose = false) {
if (!to || from === to || (from.column === to.column && from.row === to.row )){
if (this.data[from.row] && this.data[from.row][from.column]){
return this.data[from.row][from.column].GetValue2();
}
return undefined;
}
const value = [];
if (transpose){
for ( let c = from.column; c <= to.column; c++ ){
const column = [];
for ( let r = from.row; r <= to.row; r++ ){
if (this.data[r] && this.data[r][c]) column.push(this.data[r][c].GetValue2());
else column.push(undefined);
}
value.push(column);
}
}
else {
for ( let r = from.row; r <= to.row; r++ ){
const row = [];
for ( let c = from.column; c <= to.column; c++ ){
if (this.data[r] && this.data[r][c]) row.push(this.data[r][c].GetValue2());
else row.push(undefined);
}
value.push(row);
}
}
return value;
}
*/
public GetRange4(from: ICellAddress, to: ICellAddress = from, transpose = false): UnionValue {
({from, to} = this.Normalize(from, to));
if (from.row === to.row && from.column === to.column) {
if (this.data[from.row] && this.data[from.row][from.column]){
return this.data[from.row][from.column].GetValue4();
}
return { value: undefined, type: ValueType.undefined };
}
const value: UnionValue[][] = [];
if (transpose){
for ( let c = from.column; c <= to.column; c++ ){
const column: UnionValue[] = [];
for ( let r = from.row; r <= to.row; r++ ){
if (this.data[r] && this.data[r][c]) column.push(this.data[r][c].GetValue4());
else column.push({type: ValueType.undefined});
}
value.push(column);
}
}
else {
for ( let r = from.row; r <= to.row; r++ ){
const row: UnionValue[] = [];
for ( let c = from.column; c <= to.column; c++ ){
if (this.data[r] && this.data[r][c]) row.push(this.data[r][c].GetValue4());
else row.push({type: ValueType.undefined});
}
value.push(row);
}
}
return {type: ValueType.array, value};
}
/* *
* apply function to address/area
* @deprecated - use Apply2
* /
public Apply(area: Area|ICellAddress, f: (cell: Cell, c?: number, r?: number) => void, create_missing_cells = false): void {
// allow single address
if (IsCellAddress(area)) {
area = new Area(area);
}
// why not just cap? (...)
if (area.entire_column || area.entire_row) {
throw new Error(`don't iterate infinite cells`);
}
// these are accessors so we don't want them in the loop
const start = area.start;
const end = area.end;
if (create_missing_cells){
for ( let r = start.row; r <= end.row; r++ ){
if (!this.data[r]) this.data[r] = [];
const row = this.data[r];
for ( let c = start.column; c <= end.column; c++ ){
if (!row[c]) row[c] = new Cell();
f(row[c], c, r);
}
}
}
else {
// we can loop over indexes that don't exist, just check for existence
for ( let r = start.row; r <= end.row; r++ ){
if (this.data[r]){
const row = this.data[r];
for ( let c = start.column; c <= end.column; c++ ){
if (row[c]) f(row[c], c, r);
}
}
}
}
}
*/
/* *
* as a replacement for the Apply functions. testing
*
* (1) it seems like almost no one uses the cell address, so returning
* it just adds a wasteful destructuring to every loop iteration.
*
* actually there's exactly one, and we could work around it. but
* it seems like it might be useful in some cases, so... ?
*
* (2) can we consolidate with the IterateAll method (named Iterate)?
* that would only work if we had the same return type, meaning either
* drop the address from this one or add it to the other one
*
* @deprecated use Iterate, if possible
*
* /
public *IterateArea(area: Area|ICellAddress, create_missing_cells = false) {
// allow single address
if (IsCellAddress(area)) {
area = new Area(area);
}
// why not just cap? (...)
if (area.entire_column || area.entire_row) {
throw new Error(`don't iterate infinite cells`);
}
const start = area.start;
const end = area.end;
if (create_missing_cells){
for ( let r = start.row; r <= end.row; r++ ){
if (!this.data[r]) this.data[r] = [];
const row = this.data[r];
for ( let c = start.column; c <= end.column; c++ ){
if (!row[c]) row[c] = new Cell();
yield { row: r, column: c, cell: row[c] };
}
}
}
else {
// we can loop over indexes that don't exist, just check for existence
for ( let r = start.row; r <= end.row; r++ ){
if (this.data[r]){
const row = this.data[r];
for ( let c = start.column; c <= end.column; c++ ){
if (row[c]) {
yield { row: r, column: c, cell: row[c] };
}
}
}
}
}
}
*/
/* *
* apply function to address/area
*
* this version lets you abort by returning false from the callback function
* /
public Apply2(area: Area|ICellAddress, func: (cell: Cell, c?: number, r?: number) => boolean, create_missing_cells = false): void {
// allow single address
if (IsCellAddress(area)) {
area = new Area(area);
}
// why not just cap? (...)
if (area.entire_column || area.entire_row) {
throw new Error(`don't iterate infinite cells`);
}
// these are accessors so we don't want them in the loop
const start = area.start;
const end = area.end;
if (create_missing_cells){
for ( let r = start.row; r <= end.row; r++ ){
if (!this.data[r]) this.data[r] = [];
const row = this.data[r];
for ( let c = start.column; c <= end.column; c++ ){
if (!row[c]) row[c] = new Cell();
if (!func(row[c], c, r)) {
return;
}
}
}
}
else {
// we can loop over indexes that don't exist, just check for existence
for ( let r = start.row; r <= end.row; r++ ){
if (this.data[r]){
const row = this.data[r];
for ( let c = start.column; c <= end.column; c++ ){
if (row[c]) {
if (!func(row[c], c, r)) {
return;
}
}
}
}
}
}
}
*/
/**
* set area. shortcut to reduce overhead. consolidates single value
* and array value methods, although the implementation is separate.
*
* watch out for typed arrays, which do not satisfy Array.isArray
*
* when would this function get a 1D typed array? can't figure that out.
* just drop for the time being.
*
*/
public SetArea(area: Area, values: CellValue|CellValue[][]): void {
if (ArrayBuffer.isView(values)) {
throw new Error('ABIV');
}
if (Array.isArray(values)) { // || ArrayBuffer.isView(values)) {
for (let r = area.start.row, i = 0; r <= area.end.row; r++, i++) {
if (!this.data[r]) this.data[r] = [];
const row = this.data[r];
if (values[i]) {
for (let c = area.start.column, j = 0; c <= area.end.column; c++, j++) {
if (!row[c]) row[c] = new Cell();
row[c].Set(values[i][j]); // undefined should be implicit
}
}
}
}
else {
const value_type = GetValueType(values); // otherwise we'd just call it every time
for (let r = area.start.row; r <= area.end.row; r++) {
if (!this.data[r]) this.data[r] = [];
const row = this.data[r];
for (let c = area.start.column; c <= area.end.column; c++) {
if (!row[c]) row[c] = new Cell();
row[c].Set(values, value_type);
}
}
}
this.rows_ = Math.max(this.rows_, area.end.row + 1);
this.columns_ = Math.max(this.columns_, area.end.column + 1);
}
/**
* yet another iterator, this one returns cell and address.
* iterates all cells; does not create missing cells.
*
* UPDATE: adding area parameter; not shrinking it (don't call w/ infinities)
*/
public *IterateRC(area?: IArea, create_missing_cells = false) {
if (!area && create_missing_cells) {
area = new Area({
row: 0,
column: 0,
}, {
row: this.rows_ - 1,
column: this.columns - 1,
});
}
if (area) {
if (create_missing_cells) {
for (let row = area.start.row; row <= area.end.row; row++) {
if (!this.data[row]) this.data[row] = [];
const block = this.data[row];
for (let column = area.start.column; column <= area.end.column; column++) {
if (!block[column]) {
block[column] = new Cell();
}
yield { cell: block[column], row, column };
}
}
}
else {
for (let row = area.start.row; row <= area.end.row; row++) {
const block = this.data[row];
if (block) {
for (let column = area.start.column; column <= area.end.column; column++) {
const cell = block[column];
if (cell) {
yield { cell, row, column };
}
}
}
}
}
}
else {
for (const [row, r] of this.data.entries()) {
if (r) {
for (const [column, cell] of r.entries()) {
if (cell) {
yield { cell, row, column };
}
}
}
}
}
}
/**
* replacement for old callback iterator. this is a composite
* of iterating all and iterating an area. I want to remove the
* other method, but there are still some calls that use the
* cell address.
*
* Q: is it possible to use Symbol.iterator with arguments?
* A: apparently it is, but how would you call it? (...)
*/
public *Iterate(area?: Area|ICellAddress, create_missing_cells = false) {
// special case. normally iterating over all cells
// doesn't create missing, so we use a simpler loop.
if (!area && create_missing_cells) {
area = new Area({
row: 0,
column: 0,
}, {
row: this.rows_ - 1,
column: this.columns - 1,
});
}
// if we have an area, iterate over the area. we need indexes.
if (area) {
// allow single address
if (IsCellAddress(area)) {
area = new Area(area);
}
// why not just cap? (...)
// if (area.entire_column || area.entire_row) {
// throw new Error(`don't iterate infinite cells`);
//}
if (area.entire_column || area.entire_row) {
area = new Area(area.start, area.end);
if (area.start.column === Infinity) {
area.start.column = 0;
area.end.column = this.columns_ - 1;
}
if (area.start.row === Infinity) {
area.start.row = 0;
area.end.row = this.rows_ - 1;
}
}
const start = area.start;
const end = area.end;
if (create_missing_cells){
for ( let r = start.row; r <= end.row; r++ ){
if (!this.data[r]) this.data[r] = [];
const row = this.data[r];
for ( let c = start.column; c <= end.column; c++ ){
if (!row[c]) row[c] = new Cell();
yield row[c]; // { row: r, column: c, cell: row[c] };
}
}
}
else {
// we can loop over indexes that don't exist, just check for existence
for ( let r = start.row; r <= end.row; r++ ){
if (this.data[r]){
const row = this.data[r];
for ( let c = start.column; c <= end.column; c++ ){
if (row[c]) {
yield row[c]; // { row: r, column: c, cell: row[c] };
}
}
}
}
}
}
else {
// no area; just iterate all cells. implicitly skip undefined cells.
for (const row of this.data) {
if (row) {
for (const cell of row) {
if (cell) {
yield cell;
}
}
}
}
}
}
/* *
* iterates over all cells (using loops) and runs function per-cell.
* FIXME: switch to indexing on empty indexes? (...)
*
* removed in favor of generator-based function
*
* /
public IterateAll(func: (cell: Cell) => void){
/ *
const row_keys = Object.keys(this.data);
for (const row of row_keys){
const n_row = Number(row) || 0;
const column_keys = Object.keys(this.data[n_row]);
for (const column_key of column_keys){
f(this.data[n_row][Number(column_key)]);
}
}
* /
for (const row of this.data) {
if (row) {
for (const cell of row) {
if (cell) {
func(cell);
}
}
}
}
}
*/
/** moved from sheet, so we can do it non-functional style (for perf) */
public FlushCellStyles() {
for (const row of this.data) {
if (row) {
for (const cell of row) {
if (cell) {
cell.FlushStyle();
}
}
}
}
}
/** moved from sheet, so we can do it non-functional style (for perf) */
public FlushCachedValues() {
for (const row of this.data) {
if (row) {
for (const cell of row) {
if (cell) {
cell.FlushCache();
}
}
}
}
}
}