Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions demo/data.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,8 @@ export const demoData = {
C5: "42",
C7: "3",
C9: "= SUM( C4:C5 )",
C10: "=SUM(C4:C7)",
C11: "=-(3 + C7 *SUM(C4:C7))",
C10: "=SUM(MyNamedRange)",
C11: "=-(3 + C7 *SUM(MyNamedRange))",
C12: "=SUM(C9:C11)",
C14: "=C14",
C15: "=(+",
Expand Down Expand Up @@ -3832,6 +3832,13 @@ export const demoData = {
},
pivotNextId: 2,
customTableStyles: {},
namedRanges: [
{
rangeName: "MyNamedRange",
zone: { left: 2, right: 2, top: 3, bottom: 6 },
sheetId: "sh1",
},
],
};

// Performance dataset
Expand Down
30 changes: 30 additions & 0 deletions packages/o-spreadsheet-engine/src/collaborative/ot/ot_specific.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
CreateTableCommand,
DeleteChartCommand,
DeleteFigureCommand,
DeleteNamedRangeCommand,
DeleteSheetCommand,
DuplicatePivotCommand,
FoldHeaderGroupCommand,
Expand All @@ -33,6 +34,7 @@ import {
UpdateCarouselCommand,
UpdateChartCommand,
UpdateFigureCommand,
UpdateNamedRangeCommand,
UpdatePivotCommand,
UpdateTableCommand,
} from "../../types/commands";
Expand Down Expand Up @@ -107,6 +109,12 @@ otRegistry.addTransformation(
pivotZoneTransformation
);

otRegistry.addTransformation(
"UPDATE_NAMED_RANGE",
["UPDATE_NAMED_RANGE", "DELETE_NAMED_RANGE"],
updateNamedRangeTransformation
);

function pivotZoneTransformation(
toTransform: AddPivotCommand | UpdatePivotCommand,
executed: AddColumnsRowsCommand | RemoveColumnsRowsCommand
Expand Down Expand Up @@ -348,3 +356,25 @@ function groupHeadersTransformation(

return { ...toTransform, start: Math.min(...results), end: Math.max(...results) };
}

function updateNamedRangeTransformation(
toTransform: UpdateNamedRangeCommand | DeleteNamedRangeCommand,
executed: UpdateNamedRangeCommand
): UpdateNamedRangeCommand | DeleteNamedRangeCommand | undefined {
if (executed.newRangeName === executed.oldRangeName) {
return toTransform;
}
if (
toTransform.type === "DELETE_NAMED_RANGE" &&
toTransform.rangeName === executed.oldRangeName
) {
return { ...toTransform, rangeName: executed.newRangeName };
}
if (
toTransform.type === "UPDATE_NAMED_RANGE" &&
toTransform.oldRangeName === executed.oldRangeName
) {
return { ...toTransform, oldRangeName: executed.newRangeName };
}
return executed;
}
4 changes: 3 additions & 1 deletion packages/o-spreadsheet-engine/src/formulas/compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,9 @@ function compileTokensOrThrow(tokens: Token[]): CompiledFormula {
}
case "SYMBOL":
const symbolIndex = symbols.indexOf(ast.value);
return code.return(`getSymbolValue(this.symbols[${symbolIndex}])`);
return code.return(
`getSymbolValue(this.symbols[${symbolIndex}], ${hasRange}, ${isMeta}, ref, range, ctx)`
);
case "EMPTY":
return code.return("undefined");
}
Expand Down
14 changes: 9 additions & 5 deletions packages/o-spreadsheet-engine/src/formulas/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,11 +188,7 @@ function parseOperand(tokens: TokenList): AST {
case "SYMBOL":
const value = current.value;
const nextToken = tokens.current;
if (
nextToken?.type === "LEFT_PAREN" &&
functionRegex.test(current.value) &&
value === unquote(value, "'")
) {
if (isFuncallToken(current, nextToken)) {
const { args, rightParen } = parseFunctionArgs(tokens);
return {
type: "FUNCALL",
Expand Down Expand Up @@ -459,3 +455,11 @@ export function mapAst<T extends AST["type"]>(
return ast;
}
}

export function isFuncallToken(currentToken: Token, nextToken: Token | undefined) {
return (
nextToken?.type === "LEFT_PAREN" &&
functionRegex.test(currentToken.value) &&
currentToken.value === unquote(currentToken.value, "'")
);
}
5 changes: 3 additions & 2 deletions packages/o-spreadsheet-engine/src/helpers/range.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ export function isFullRowRange(range: Range): boolean {

export function getRangeString(
range: Range,
forSheetId: UID,
forSheetId: UID | undefined,
getSheetName: (sheetId: UID) => string,
options: RangeStringOptions = { useBoundedReference: false, useFixedReference: false }
): string {
Expand All @@ -133,7 +133,8 @@ export function getRangeString(
if (range.zone.left < 0 || range.zone.top < 0) {
return CellErrorType.InvalidReference;
}
const prefixSheet = range.sheetId !== forSheetId || range.invalidSheetName || range.prefixSheet;
const prefixSheet =
!forSheetId || range.sheetId !== forSheetId || range.invalidSheetName || range.prefixSheet;
let sheetName: string = "";
if (prefixSheet) {
if (range.invalidSheetName) {
Expand Down
1 change: 1 addition & 0 deletions packages/o-spreadsheet-engine/src/migrations/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,7 @@ export function createEmptyWorkbookData(sheetName = "Sheet1"): WorkbookData {
pivots: {},
pivotNextId: 1,
customTableStyles: {},
namedRanges: [],
};
}

Expand Down
210 changes: 210 additions & 0 deletions packages/o-spreadsheet-engine/src/plugins/core/named_range.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
import { isFuncallToken } from "../../formulas/parser";
import { Token } from "../../formulas/tokenizer";
import { deepEquals, isNumber, rangeReference } from "../../helpers";
import { Command, CommandResult, CoreCommand } from "../../types/commands";
import { DEFAULT_LOCALE } from "../../types/locale";
import {
AdaptSheetName,
ApplyRangeChange,
NamedRange,
UID,
UnboundedZone,
Zone,
} from "../../types/misc";
import { WorkbookData } from "../../types/workbook_data";
import { CorePlugin } from "../core_plugin";

interface NamedRangeState {
readonly namedRanges: Array<NamedRange>;
}

const invalidNamedRangeCharacterRegex = /[^a-zA-Z0-9_.]/;

export class NamedRangesPlugin extends CorePlugin<NamedRangeState> implements NamedRangeState {
static getters = ["getNamedRange", "getNamedRangeFromZone", "getNamedRanges"] as const;

readonly namedRanges: Array<NamedRange> = [];

adaptRanges(applyChange: ApplyRangeChange, sheetId: UID, adaptSheetName: AdaptSheetName) {
const newNamedRanges: Array<NamedRange> = [];
let hasChanges = false;
for (const namedRange of this.namedRanges) {
const change = applyChange(namedRange.range);
switch (change.changeType) {
case "REMOVE":
hasChanges = true;
break;
case "RESIZE":
case "MOVE":
case "CHANGE":
hasChanges = true;
newNamedRanges.push({ ...namedRange, range: change.range });
break;
case "NONE":
newNamedRanges.push(namedRange);
}
}
if (hasChanges) {
this.history.update("namedRanges", newNamedRanges);
}
}

allowDispatch(cmd: Command) {
switch (cmd.type) {
case "CREATE_NAMED_RANGE":
return this.checkValidNewNamedRangeName(cmd.rangeName);
case "UPDATE_NAMED_RANGE":
return this.checkValidations(
cmd,
() => this.checkNamedRangeExists(cmd.oldRangeName),
() =>
cmd.newRangeName !== cmd.oldRangeName
? this.checkValidNewNamedRangeName(cmd.newRangeName)
: CommandResult.Success
);
case "DELETE_NAMED_RANGE":
return this.checkNamedRangeExists(cmd.rangeName);
}
return CommandResult.Success;
}

handle(cmd: CoreCommand) {
switch (cmd.type) {
case "CREATE_NAMED_RANGE": {
const range = this.getters.getRangeFromRangeData(cmd.ranges[0]);
const newNamedRanges = [...this.namedRanges, { rangeName: cmd.rangeName, range }];
this.history.update("namedRanges", newNamedRanges);
break;
}
case "UPDATE_NAMED_RANGE": {
const index = this.getNamedRangeIndex(cmd.oldRangeName);
if (index !== -1) {
const range = this.getters.getRangeFromRangeData(cmd.ranges[0]);
const newNamedRanges = [...this.namedRanges];
newNamedRanges[index] = { rangeName: cmd.newRangeName, range };
this.history.update("namedRanges", newNamedRanges);
if (cmd.oldRangeName !== cmd.newRangeName) {
this.renameNamedRangeInFormulas(cmd.oldRangeName, cmd.newRangeName);
}
}
break;
}
case "DELETE_NAMED_RANGE": {
const index = this.getNamedRangeIndex(cmd.rangeName);
if (index !== -1) {
const newNamedRanges = [...this.namedRanges];
newNamedRanges.splice(index, 1);
this.history.update("namedRanges", newNamedRanges);
}
break;
}
}
}

getNamedRange(rangeName: UID): NamedRange | undefined {
return this.namedRanges[this.getNamedRangeIndex(rangeName)];
}

getNamedRangeFromZone(sheetId: UID, zone: Zone | UnboundedZone): NamedRange | undefined {
for (const namedRange of this.namedRanges) {
const range = namedRange.range;
if (range.sheetId === sheetId && deepEquals(range.unboundedZone, zone)) {
return namedRange;
}
}
return undefined;
}

getNamedRanges(): NamedRange[] {
return this.namedRanges;
}

private getNamedRangeIndex(rangeName: UID): number {
return this.namedRanges.findIndex(
(r) => r && r.rangeName.toLowerCase() === rangeName.toLowerCase()
);
}

private renameNamedRangeInFormulas(oldName: string, newName: string) {
const lowerCaseOldName = oldName.toLowerCase();
const isOldNamedRangeToken = (token: Token, nextToken: Token | undefined) => {
return (
token.type === "SYMBOL" &&
token.value.toLowerCase() === lowerCaseOldName &&
!isFuncallToken(token, nextToken)
);
};

for (const sheetId of this.getters.getSheetIds()) {
const cells = this.getters.getCells(sheetId);
for (const cellId in cells) {
const cell = cells[cellId];
if (!cell.isFormula) {
continue;
}

const tokens = cell.compiledFormula.tokens;
let newContent = "";
let hasChanged = false;
for (let i = 0; i < tokens.length; i++) {
if (isOldNamedRangeToken(tokens[i], tokens[i + 1])) {
hasChanged = true;
newContent += newName;
} else {
newContent += tokens[i].value;
}
}

if (hasChanged) {
this.dispatch("UPDATE_CELL", {
...this.getters.getCellPosition(cellId),
content: newContent,
});
}
}
}
}

import(data: WorkbookData) {
for (const namedRangeData of data.namedRanges || []) {
this.namedRanges.push({
rangeName: namedRangeData.rangeName,
range: this.getters.getRangeFromZone(namedRangeData.sheetId, namedRangeData.zone),
});
}
}

export(data: WorkbookData) {
data.namedRanges = [];
for (const namedRange of this.namedRanges) {
data.namedRanges.push({
rangeName: namedRange.rangeName,
zone: namedRange.range.unboundedZone,
sheetId: namedRange.range.sheetId,
});
}
}

private checkValidNewNamedRangeName(name: string): CommandResult {
if (this.getNamedRange(name)) {
return CommandResult.NamedRangeNameAlreadyExists;
}

if (invalidNamedRangeCharacterRegex.test(name) || isNumber(name, DEFAULT_LOCALE)) {
return CommandResult.NamedRangeNameWithInvalidCharacter;
}

if (rangeReference.test(name)) {
return CommandResult.NamedRangeNameLooksLikeCellReference;
}

return CommandResult.Success;
}

private checkNamedRangeExists(name: string): CommandResult {
if (!this.getNamedRange(name)) {
return CommandResult.NamedRangeNotFound;
}
return CommandResult.Success;
}
}
4 changes: 2 additions & 2 deletions packages/o-spreadsheet-engine/src/plugins/core/range.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ export class RangeAdapter implements CommandHandler<CoreCommand> {
/**
* Gets the string that represents the range as it is at the moment of the call.
* The string will be prefixed with the sheet name if the call specified a sheet id in `forSheetId`
* different than the sheet on which the range has been created.
* different than the sheet on which the range has been created or if `forSheetId` is not specified.
*
* @param range the range (received from getRangeFromXC or getRangeFromZone)
* @param forSheetId the id of the sheet where the range string is supposed to be used.
Expand All @@ -216,7 +216,7 @@ export class RangeAdapter implements CommandHandler<CoreCommand> {
*/
getRangeString(
range: Range,
forSheetId: UID,
forSheetId?: UID,
options: RangeStringOptions = { useBoundedReference: false, useFixedReference: false }
): string {
if (!range) {
Expand Down
4 changes: 3 additions & 1 deletion packages/o-spreadsheet-engine/src/plugins/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { HeaderSizePlugin } from "./core/header_size";
import { HeaderVisibilityPlugin } from "./core/header_visibility";
import { ImagePlugin } from "./core/image";
import { MergePlugin } from "./core/merge";
import { NamedRangesPlugin } from "./core/named_range";
import { PivotCorePlugin } from "./core/pivot";
import { SettingsPlugin } from "./core/settings";
import { SheetPlugin } from "./core/sheet";
Expand Down Expand Up @@ -77,7 +78,8 @@ export const corePluginRegistry = new Registry<CorePluginConstructor>()
.add("image", ImagePlugin)
.add("pivot_core", PivotCorePlugin)
.add("spreadsheet_pivot_core", SpreadsheetPivotCorePlugin)
.add("tableStyle", TableStylePlugin);
.add("tableStyle", TableStylePlugin)
.add("named_ranges", NamedRangesPlugin);

// Plugins which handle a specific feature, without handling any core commands
export const featurePluginRegistry = new Registry<UIPluginConstructor>()
Expand Down
Loading