mirror of
https://github.com/RGBCube/serenity
synced 2025-07-26 02:17:34 +00:00
Spreadsheet: Make it possible to refer to ranges in other sheets
Now the range A0:C4 in a sheet named "foo" can be represented as: R`sheet("foo"):A0:C4` This makes it possible to do cross-sheet lookups and more.
This commit is contained in:
parent
2104e9a6e4
commit
746b8ec8de
4 changed files with 93 additions and 20 deletions
|
@ -67,7 +67,11 @@ class Position {
|
||||||
point = current_point.up(1)
|
point = current_point.up(1)
|
||||||
)
|
)
|
||||||
current_point = point;
|
current_point = point;
|
||||||
return R(current_point.name + ":" + up_one.name);
|
|
||||||
|
const sheetName = Object.is(this.sheet, thisSheet)
|
||||||
|
? ""
|
||||||
|
: `sheet(${JSON.stringify(this.sheet.name)}):`;
|
||||||
|
return R(sheetName + current_point.name + ":" + up_one.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
range_down() {
|
range_down() {
|
||||||
|
@ -75,7 +79,11 @@ class Position {
|
||||||
let current_point = down_one;
|
let current_point = down_one;
|
||||||
for (let point = current_point.down(1); point.value() !== ""; point = current_point.down(1))
|
for (let point = current_point.down(1); point.value() !== ""; point = current_point.down(1))
|
||||||
current_point = point;
|
current_point = point;
|
||||||
return R(current_point.name + ":" + down_one.name);
|
|
||||||
|
const sheetName = Object.is(this.sheet, thisSheet)
|
||||||
|
? ""
|
||||||
|
: `sheet(${JSON.stringify(this.sheet.name)}):`;
|
||||||
|
return R(sheetName + current_point.name + ":" + down_one.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
range_left() {
|
range_left() {
|
||||||
|
@ -88,7 +96,11 @@ class Position {
|
||||||
point = current_point.left(1)
|
point = current_point.left(1)
|
||||||
)
|
)
|
||||||
current_point = point;
|
current_point = point;
|
||||||
return R(current_point.name + ":" + left_one.name);
|
|
||||||
|
const sheetName = Object.is(this.sheet, thisSheet)
|
||||||
|
? ""
|
||||||
|
: `sheet(${JSON.stringify(this.sheet.name)}):`;
|
||||||
|
return R(sheetName + current_point.name + ":" + left_one.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
range_right() {
|
range_right() {
|
||||||
|
@ -100,7 +112,11 @@ class Position {
|
||||||
point = current_point.right(1)
|
point = current_point.right(1)
|
||||||
)
|
)
|
||||||
current_point = point;
|
current_point = point;
|
||||||
return R(current_point.name + ":" + right_one.name);
|
|
||||||
|
const sheetName = Object.is(this.sheet, thisSheet)
|
||||||
|
? ""
|
||||||
|
: `sheet(${JSON.stringify(this.sheet.name)}):`;
|
||||||
|
return R(sheetName + current_point.name + ":" + right_one.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
with_column(value) {
|
with_column(value) {
|
||||||
|
@ -124,7 +140,9 @@ class Position {
|
||||||
}
|
}
|
||||||
|
|
||||||
toString() {
|
toString() {
|
||||||
return `<Cell at ${this.name}>`;
|
return `<Cell at ${this.name}${
|
||||||
|
Object.is(this.sheet, thisSheet) ? "" : ` in sheet(${JSON.stringify(this.sheet.name)})`
|
||||||
|
}>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -276,7 +294,15 @@ class Ranges extends CommonRange {
|
||||||
}
|
}
|
||||||
|
|
||||||
class Range extends CommonRange {
|
class Range extends CommonRange {
|
||||||
constructor(startingColumnName, endingColumnName, startingRow, endingRow, columnStep, rowStep) {
|
constructor(
|
||||||
|
startingColumnName,
|
||||||
|
endingColumnName,
|
||||||
|
startingRow,
|
||||||
|
endingRow,
|
||||||
|
columnStep,
|
||||||
|
rowStep,
|
||||||
|
sheet
|
||||||
|
) {
|
||||||
super();
|
super();
|
||||||
// using == to account for '0' since js will parse `+'0'` to 0
|
// using == to account for '0' since js will parse `+'0'` to 0
|
||||||
if (columnStep == 0 || rowStep == 0)
|
if (columnStep == 0 || rowStep == 0)
|
||||||
|
@ -292,6 +318,7 @@ class Range extends CommonRange {
|
||||||
this.columnStep = columnStep ?? 1;
|
this.columnStep = columnStep ?? 1;
|
||||||
this.rowStep = rowStep ?? 1;
|
this.rowStep = rowStep ?? 1;
|
||||||
this.spansEntireColumn = endingRow === undefined;
|
this.spansEntireColumn = endingRow === undefined;
|
||||||
|
this.sheet = sheet;
|
||||||
if (!this.spansEntireColumn && startingRow === undefined)
|
if (!this.spansEntireColumn && startingRow === undefined)
|
||||||
throw new Error("A Range with a defined end row must also have a defined start row");
|
throw new Error("A Range with a defined end row must also have a defined start row");
|
||||||
|
|
||||||
|
@ -299,32 +326,32 @@ class Range extends CommonRange {
|
||||||
}
|
}
|
||||||
|
|
||||||
first() {
|
first() {
|
||||||
return new Position(this.startingColumnName, this.startingRow);
|
return new Position(this.startingColumnName, this.startingRow, this.sheet);
|
||||||
}
|
}
|
||||||
|
|
||||||
forEach(callback) {
|
forEach(callback) {
|
||||||
const ranges = [];
|
const ranges = [];
|
||||||
let startingColumnIndex = thisSheet.column_index(this.startingColumnName);
|
let startingColumnIndex = this.sheet.column_index(this.startingColumnName);
|
||||||
let endingColumnIndex = thisSheet.column_index(this.endingColumnName);
|
let endingColumnIndex = this.sheet.column_index(this.endingColumnName);
|
||||||
let columnDistance = endingColumnIndex - startingColumnIndex;
|
let columnDistance = endingColumnIndex - startingColumnIndex;
|
||||||
for (
|
for (
|
||||||
let columnOffset = 0;
|
let columnOffset = 0;
|
||||||
columnOffset <= columnDistance;
|
columnOffset <= columnDistance;
|
||||||
columnOffset += this.columnStep
|
columnOffset += this.columnStep
|
||||||
) {
|
) {
|
||||||
const columnName = thisSheet.column_arithmetic(this.startingColumnName, columnOffset);
|
const columnName = this.sheet.column_arithmetic(this.startingColumnName, columnOffset);
|
||||||
ranges.push({
|
ranges.push({
|
||||||
column: columnName,
|
column: columnName,
|
||||||
rowStart: this.startingRow,
|
rowStart: this.startingRow,
|
||||||
rowEnd: this.spansEntireColumn
|
rowEnd: this.spansEntireColumn
|
||||||
? thisSheet.get_column_bound(columnName)
|
? this.sheet.get_column_bound(columnName)
|
||||||
: this.endingRow,
|
: this.endingRow,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
outer: for (const range of ranges) {
|
outer: for (const range of ranges) {
|
||||||
for (let row = range.rowStart; row <= range.rowEnd; row += this.rowStep) {
|
for (let row = range.rowStart; row <= range.rowEnd; row += this.rowStep) {
|
||||||
if (callback(new Position(range.column, row)) === Break) break outer;
|
if (callback(new Position(range.column, row, this.sheet)) === Break) break outer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -338,8 +365,8 @@ class Range extends CommonRange {
|
||||||
}
|
}
|
||||||
|
|
||||||
normalize() {
|
normalize() {
|
||||||
const startColumnIndex = thisSheet.column_index(this.startingColumnName);
|
const startColumnIndex = this.sheet.column_index(this.startingColumnName);
|
||||||
const endColumnIndex = thisSheet.column_index(this.endingColumnName);
|
const endColumnIndex = this.sheet.column_index(this.endingColumnName);
|
||||||
if (startColumnIndex > endColumnIndex) {
|
if (startColumnIndex > endColumnIndex) {
|
||||||
const temp = this.startingColumnName;
|
const temp = this.startingColumnName;
|
||||||
this.startingColumnName = this.endingColumnName;
|
this.startingColumnName = this.endingColumnName;
|
||||||
|
@ -359,11 +386,15 @@ class Range extends CommonRange {
|
||||||
const endingRow = this.endingRow ?? "";
|
const endingRow = this.endingRow ?? "";
|
||||||
const showSteps = this.rowStep !== 1 || this.columnStep !== 1;
|
const showSteps = this.rowStep !== 1 || this.columnStep !== 1;
|
||||||
const steps = showSteps ? `:${this.columnStep}:${this.rowStep}` : "";
|
const steps = showSteps ? `:${this.columnStep}:${this.rowStep}` : "";
|
||||||
return `R\`${this.startingColumnName}${this.startingRow}:${this.endingColumnName}${endingRow}${steps}\``;
|
const sheetName = Object.is(thisSheet, this.sheet)
|
||||||
|
? ""
|
||||||
|
: `sheet(${JSON.stringify(this.sheet.name)}):`;
|
||||||
|
return `R\`${sheetName}${this.startingColumnName}${this.startingRow}:${this.endingColumnName}${endingRow}${steps}\``;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const R_FORMAT = /^([a-zA-Z_]+)(?:(\d+):([a-zA-Z_]+)(\d+)?(?::(\d+):(\d+))?)?$/;
|
const R_FORMAT =
|
||||||
|
/^(?:sheet\(("(?:[^"]|\\")*")\):)?([a-zA-Z_]+)(?:(\d+):([a-zA-Z_]+)(\d+)?(?::(\d+):(\d+))?)?$/;
|
||||||
function R(fmt, ...args) {
|
function R(fmt, ...args) {
|
||||||
if (args.length !== 0) throw new TypeError("R`` format must be a literal");
|
if (args.length !== 0) throw new TypeError("R`` format must be a literal");
|
||||||
// done because:
|
// done because:
|
||||||
|
@ -372,11 +403,17 @@ function R(fmt, ...args) {
|
||||||
// myFunc`ABC` => "["ABC"]"
|
// myFunc`ABC` => "["ABC"]"
|
||||||
if (Array.isArray(fmt)) fmt = fmt[0];
|
if (Array.isArray(fmt)) fmt = fmt[0];
|
||||||
if (!R_FORMAT.test(fmt))
|
if (!R_FORMAT.test(fmt))
|
||||||
throw new Error("Invalid Format. Expected Format: R`A` or R`A0:A1` or R`A0:A2:1:2`");
|
throw new Error(
|
||||||
// Format: Col(Row:Col(Row)?(:ColStep:RowStep)?)?
|
'Invalid Format. Expected Format: R`A` or R`A0:A1` or R`A0:A2:1:2` or R`sheet("sheetName"):...`'
|
||||||
|
);
|
||||||
|
// Format: (sheet("sheetName"):)?Col(Row:Col(Row)?(:ColStep:RowStep)?)?
|
||||||
// Ignore the first element of the match array as that will be the whole match.
|
// Ignore the first element of the match array as that will be the whole match.
|
||||||
const [, ...matches] = fmt.match(R_FORMAT);
|
const [, ...matches] = fmt.match(R_FORMAT);
|
||||||
const [startCol, startRow, endCol, endRow, colStep, rowStep] = matches;
|
const [sheetExpression, startCol, startRow, endCol, endRow, colStep, rowStep] = matches;
|
||||||
|
const sheetFromName = name => {
|
||||||
|
if (name == null || name === "") return thisSheet;
|
||||||
|
return sheet(JSON.parse(name));
|
||||||
|
};
|
||||||
return new Range(
|
return new Range(
|
||||||
startCol,
|
startCol,
|
||||||
endCol ?? startCol,
|
endCol ?? startCol,
|
||||||
|
@ -384,7 +421,8 @@ function R(fmt, ...args) {
|
||||||
// Don't make undefined an integer, because then it becomes 0.
|
// Don't make undefined an integer, because then it becomes 0.
|
||||||
!!endRow ? integer(endRow) : endRow,
|
!!endRow ? integer(endRow) : endRow,
|
||||||
integer(colStep ?? 1),
|
integer(colStep ?? 1),
|
||||||
integer(rowStep ?? 1)
|
integer(rowStep ?? 1),
|
||||||
|
sheetFromName(sheetExpression)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -154,6 +154,7 @@ void SheetGlobalObject::initialize_global_object()
|
||||||
define_native_function("column_arithmetic", column_arithmetic, 2, attr);
|
define_native_function("column_arithmetic", column_arithmetic, 2, attr);
|
||||||
define_native_function("column_index", column_index, 1, attr);
|
define_native_function("column_index", column_index, 1, attr);
|
||||||
define_native_function("get_column_bound", get_column_bound, 1, attr);
|
define_native_function("get_column_bound", get_column_bound, 1, attr);
|
||||||
|
define_native_accessor("name", get_name, nullptr, attr);
|
||||||
}
|
}
|
||||||
|
|
||||||
void SheetGlobalObject::visit_edges(Visitor& visitor)
|
void SheetGlobalObject::visit_edges(Visitor& visitor)
|
||||||
|
@ -167,6 +168,17 @@ void SheetGlobalObject::visit_edges(Visitor& visitor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
JS_DEFINE_NATIVE_FUNCTION(SheetGlobalObject::get_name)
|
||||||
|
{
|
||||||
|
auto* this_object = TRY(vm.this_value(global_object).to_object(global_object));
|
||||||
|
|
||||||
|
if (!is<SheetGlobalObject>(this_object))
|
||||||
|
return vm.throw_completion<JS::TypeError>(global_object, JS::ErrorType::NotAnObjectOfType, "SheetGlobalObject");
|
||||||
|
|
||||||
|
auto sheet_object = static_cast<SheetGlobalObject*>(this_object);
|
||||||
|
return JS::js_string(global_object.heap(), sheet_object->m_sheet.name());
|
||||||
|
}
|
||||||
|
|
||||||
JS_DEFINE_NATIVE_FUNCTION(SheetGlobalObject::get_real_cell_contents)
|
JS_DEFINE_NATIVE_FUNCTION(SheetGlobalObject::get_real_cell_contents)
|
||||||
{
|
{
|
||||||
auto* this_object = TRY(vm.this_value(global_object).to_object(global_object));
|
auto* this_object = TRY(vm.this_value(global_object).to_object(global_object));
|
||||||
|
|
|
@ -39,6 +39,7 @@ public:
|
||||||
JS_DECLARE_NATIVE_FUNCTION(column_index);
|
JS_DECLARE_NATIVE_FUNCTION(column_index);
|
||||||
JS_DECLARE_NATIVE_FUNCTION(column_arithmetic);
|
JS_DECLARE_NATIVE_FUNCTION(column_arithmetic);
|
||||||
JS_DECLARE_NATIVE_FUNCTION(get_column_bound);
|
JS_DECLARE_NATIVE_FUNCTION(get_column_bound);
|
||||||
|
JS_DECLARE_NATIVE_FUNCTION(get_name);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
virtual void visit_edges(Visitor&) override;
|
virtual void visit_edges(Visitor&) override;
|
||||||
|
|
|
@ -78,6 +78,28 @@ describe("Range", () => {
|
||||||
expect(cellsVisited).toEqual(6);
|
expect(cellsVisited).toEqual(6);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("multiple sheets", () => {
|
||||||
|
const workbook = createWorkbook();
|
||||||
|
const sheet1 = createSheet(workbook, "Sheet 1");
|
||||||
|
const sheet2 = createSheet(workbook, "Sheet 2");
|
||||||
|
sheet1.makeCurrent();
|
||||||
|
|
||||||
|
sheet1.setCell("A", 0, "0");
|
||||||
|
sheet1.focusCell("A", 0);
|
||||||
|
|
||||||
|
sheet2.setCell("A", 0, "0");
|
||||||
|
sheet2.setCell("A", 10, "0");
|
||||||
|
sheet2.setCell("B", 1, "0");
|
||||||
|
sheet2.focusCell("A", 0);
|
||||||
|
|
||||||
|
expect(R).toBeDefined();
|
||||||
|
let cellsVisited = 0;
|
||||||
|
R`sheet("Sheet 2"):A0:A10`.forEach(name => {
|
||||||
|
++cellsVisited;
|
||||||
|
});
|
||||||
|
expect(cellsVisited).toEqual(11);
|
||||||
|
});
|
||||||
|
|
||||||
test("Ranges", () => {
|
test("Ranges", () => {
|
||||||
const workbook = createWorkbook();
|
const workbook = createWorkbook();
|
||||||
const sheet = createSheet(workbook, "Sheet 1");
|
const sheet = createSheet(workbook, "Sheet 1");
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue