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
88 changes: 22 additions & 66 deletions package-lock.json

Large diffs are not rendered by default.

14 changes: 14 additions & 0 deletions packages/o-spreadsheet-engine/src/helpers/format/format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,14 @@ function applyInternalNumberFormat(value: number, format: NumberInternalFormat,
return "∞" + (format.percentSymbols ? "%" : "");
}

let power = Math.floor(Math.log10(Math.abs(value)));
if (power === -Infinity) {
power = 0;
}
if (format.scientific) {
value = value / 10 ** power;
}

const multiplier = format.percentSymbols * 2 - format.magnitude * 3;
value = value * 10 ** multiplier;

Expand All @@ -253,6 +261,12 @@ function applyInternalNumberFormat(value: number, format: NumberInternalFormat,
formattedValue += locale.decimalSeparator + applyDecimalFormat(decimalDigits || "", format);
}

if (format.scientific) {
const powerAsString = Math.abs(power).toString().padStart(2, "0");
const sign = power >= 0 ? "+" : "-";
formattedValue += "e" + sign + powerAsString;
}

return formattedValue;
}

Expand Down
14 changes: 14 additions & 0 deletions packages/o-spreadsheet-engine/src/helpers/format/format_parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
FormatToken,
PercentToken,
RepeatCharToken,
ScientificToken,
StringToken,
TextPlaceholderToken,
ThousandsSeparatorToken,
Expand Down Expand Up @@ -40,11 +41,13 @@ export interface NumberInternalFormat {
| StringToken
| CharToken
| PercentToken
| ScientificToken
| ThousandsSeparatorToken
| RepeatCharToken
)[];
readonly percentSymbols: number;
readonly thousandsSeparator: boolean;
readonly scientific: boolean;
/** A thousand separator after the last digit in the format means that we divide the number by a thousand */
readonly magnitude: number;
/**
Expand All @@ -57,6 +60,7 @@ export interface NumberInternalFormat {
| StringToken
| CharToken
| PercentToken
| ScientificToken
| ThousandsSeparatorToken
| RepeatCharToken
)[];
Expand Down Expand Up @@ -151,6 +155,7 @@ function areValidNumberFormatTokens(
| DecimalPointToken
| ThousandsSeparatorToken
| PercentToken
| ScientificToken
| StringToken
| CharToken
| RepeatCharToken
Expand All @@ -161,6 +166,7 @@ function areValidNumberFormatTokens(
token.type === "DECIMAL_POINT" ||
token.type === "THOUSANDS_SEPARATOR" ||
token.type === "PERCENT" ||
token.type === "SCIENTIFIC" ||
token.type === "STRING" ||
token.type === "CHAR" ||
token.type === "REPEATED_CHAR"
Expand Down Expand Up @@ -188,6 +194,7 @@ function parseNumberFormatTokens(

let parsedPart = integerPart;
let percentSymbols = 0;
let scientific = false;
let magnitude = 0;
let lastIndexOfDigit = tokens.findLastIndex((token) => token.type === "DIGIT");
let hasThousandSeparator = false;
Expand All @@ -212,6 +219,9 @@ function parseNumberFormatTokens(
throw new Error("Multiple decimal points in a number format");
}
break;
case "SCIENTIFIC":
scientific = true;
break;
case "REPEATED_CHAR":
case "CHAR":
case "STRING":
Expand Down Expand Up @@ -247,6 +257,7 @@ function parseNumberFormatTokens(
integerPart,
decimalPart,
percentSymbols,
scientific,
thousandsSeparator: hasThousandSeparator,
magnitude,
};
Expand Down Expand Up @@ -349,6 +360,9 @@ function numberInternalFormatToTokenList(internalFormat: NumberInternalFormat):
tokens.push({ type: "DECIMAL_POINT", value: "." });
tokens.push(...internalFormat.decimalPart);
}
if (internalFormat.scientific) {
tokens.push({ type: "SCIENTIFIC", value: "e" });
}

return tokens;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ export interface PercentToken {
value: "%";
}

export interface ScientificToken {
type: "SCIENTIFIC";
value: "e";
}

export interface ThousandsSeparatorToken {
type: "THOUSANDS_SEPARATOR";
value: ",";
Expand All @@ -51,6 +56,7 @@ export type FormatToken =
| StringToken
| CharToken
| PercentToken
| ScientificToken
| ThousandsSeparatorToken
| TextPlaceholderToken
| DatePartToken
Expand All @@ -77,6 +83,7 @@ export function tokenizeFormat(str: string): FormatToken[][] {
tokenizeThousandsSeparator(chars) ||
tokenizeDecimalPoint(chars) ||
tokenizePercent(chars) ||
tokenizeScientific(chars) ||
tokenizeDatePart(chars) ||
tokenizeTextPlaceholder(chars) ||
tokenizeRepeatedChar(chars);
Expand Down Expand Up @@ -173,6 +180,14 @@ function tokenizePercent(chars: TokenizingChars): FormatToken | null {
return null;
}

function tokenizeScientific(chars: TokenizingChars): FormatToken | null {
if (chars.current === "e") {
chars.shift();
return { type: "SCIENTIFIC", value: "e" };
}
return null;
}

function tokenizeDigit(chars: TokenizingChars): FormatToken | null {
if (chars.current === "0" || chars.current === "#") {
const value = chars.current;
Expand Down
4 changes: 2 additions & 2 deletions packages/o-spreadsheet-engine/src/helpers/numbers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export const getFormulaNumberRegex = memoize(function getFormulaNumberRegex(
) {
decimalSeparator = escapeRegExp(decimalSeparator);
return new RegExp(
`(?:^-?\\d+(?:${decimalSeparator}?\\d*(?:e(\\+|-)?\\d+)?)?|^-?${decimalSeparator}\\d+)(?!\\w|!)`
`(?:^-?\\d+(?:${decimalSeparator}?\\d*(?:(E|e)(\\+|-)?\\d+)?)?|^-?${decimalSeparator}\\d+)(?!\\w|!)`
);
});

Expand All @@ -24,7 +24,7 @@ const getNumberRegex = memoize(function getNumberRegex(locale: Locale) {

const pIntegerAndDecimals = `(?:\\d+(?:${thousandsSeparator}\\d{3,})*(?:${decimalSeparator}\\d*)?)`; // pattern that match integer number with or without decimal digits
const pOnlyDecimals = `(?:${decimalSeparator}\\d+)`; // pattern that match only expression with decimal digits
const pScientificFormat = "(?:e(?:\\+|-)?\\d+)?"; // pattern that match scientific format between zero and one time (should be placed before pPercentFormat)
const pScientificFormat = "(?:(e|E)(?:\\+|-)?(?:[0-9]|[1-9][0-9]|[12][0-9]{2}|30[0-7]))?"; // pattern that match scientific format between zero and one time (should be placed before pPercentFormat)
const pPercentFormat = "(?:\\s*%)?"; // pattern that match percent symbol between zero and one time
const pNumber =
"(?:\\s*" +
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
CellErrorType,
CircularDependencyError,
SplillBlockedError,
TooBigNumberError,
} from "../../../types/errors";
import { buildCompilationParameters, CompilationParameters } from "./compilation_parameters";
import { FormulaDependencyGraph } from "./formula_dependency_graph";
Expand Down Expand Up @@ -386,10 +387,11 @@ export class Evaluator {
this.buildSafeGetSymbolValue(),
formulaPosition
);

if (!isMatrix(formulaReturn)) {
const evaluatedCell = createEvaluatedCell(
nullValueToZeroValue(formulaReturn),
typeof formulaReturn.value === "number" && Math.abs(formulaReturn.value) > Number.MAX_VALUE
? new TooBigNumberError()
: nullValueToZeroValue(formulaReturn),
this.getters.getLocale(),
cellData,
formulaPosition
Expand Down Expand Up @@ -422,7 +424,10 @@ export class Evaluator {
);
this.invalidatePositionsDependingOnSpread(formulaPosition.sheetId, resultZone);
return createEvaluatedCell(
nullValueToZeroValue(formulaReturn[0][0]),
typeof formulaReturn[0][0].value === "number" &&
Math.abs(formulaReturn[0][0].value) > Number.MAX_VALUE
? new TooBigNumberError()
: nullValueToZeroValue(formulaReturn[0][0]),
this.getters.getLocale(),
cellData
);
Expand Down Expand Up @@ -517,7 +522,10 @@ export class Evaluator {
const position = { sheetId, col: i + col, row: j + row };
const cell = this.getters.getCell(position);
const evaluatedCell = createEvaluatedCell(
nullValueToZeroValue(matrixResult[i][j]),
typeof matrixResult[i][j].value === "number" &&
Math.abs(matrixResult[i][j].value) > Number.MAX_VALUE
? new TooBigNumberError()
: nullValueToZeroValue(matrixResult[i][j]),
this.getters.getLocale(),
cell,
position
Expand Down
7 changes: 7 additions & 0 deletions packages/o-spreadsheet-engine/src/types/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export const CellErrorType = {
CircularDependency: "#CYCLE",
UnknownFunction: "#NAME?",
DivisionByZero: "#DIV/0!",
TooBigNumber: "#NUM!",
SpilledBlocked: "#SPILL!",
GenericError: "#ERROR",
NullError: "#NULL!",
Expand Down Expand Up @@ -65,3 +66,9 @@ export class DivisionByZeroError extends EvaluationError {
super(message, CellErrorType.DivisionByZero);
}
}

export class TooBigNumberError extends EvaluationError {
constructor(message = _t("Too big number")) {
super(message, CellErrorType.TooBigNumber);
}
}
6 changes: 6 additions & 0 deletions src/actions/format_actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,12 @@ export const formatNumberPercent = createFormatActionSpec({
format: "0.00%",
});

export const formatNumberScientific = createFormatActionSpec({
name: _t("Scientific"),
descriptionValue: 0.1012,
format: "0.00e",
});

export const formatNumberCurrency = createFormatActionSpec({
name: _t("Currency"),
descriptionValue: 1000.12,
Expand Down
19 changes: 13 additions & 6 deletions src/components/side_panel/more_formats/more_formats_store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,12 +125,19 @@ export class MoreFormatsStore extends SpreadsheetStore {
}

get numberFormatProposals() {
const numberFormats = ["0.00", "0", "#,##0", "#,##0.00", "0%", "0.00%", "0.00;(0.00);-"].map(
(format) => ({
label: formatValue(-1234.56, { format, locale: this.getters.getLocale() }),
format,
})
);
const numberFormats = [
"0.00",
"0",
"#,##0",
"#,##0.00",
"0%",
"0.00%",
"0.00e",
"0.00;(0.00);-",
].map((format) => ({
label: formatValue(-1234.56, { format, locale: this.getters.getLocale() }),
format,
}));

return [
{ label: _t("Automatic"), format: undefined },
Expand Down
6 changes: 6 additions & 0 deletions src/registries/menus/number_format_menu_registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ numberFormatMenuRegistry
...ACTION_FORMAT.formatNumberPercent,
id: "format_number_percent",
sequence: 30,
separator: false,
})
.add("format_number_scientific", {
...ACTION_FORMAT.formatNumberScientific,
id: "format_number_scientific",
sequence: 33,
separator: true,
})
.add("format_number_currency", {
Expand Down
41 changes: 41 additions & 0 deletions tests/evaluation/evaluation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ describe("evaluateCells", () => {
C1: "1.2e4",
C2: "1e5",
C3: "-1e3",
C4: "1E309", // too big number
D1: "1.1.1", // not a number
};
expect(evaluateGrid(grid)).toEqual({
Expand All @@ -59,6 +60,7 @@ describe("evaluateCells", () => {
C1: 12000,
C2: 100000,
C3: -1000,
C4: "1E309",
D1: "1.1.1",
});
});
Expand All @@ -76,6 +78,11 @@ describe("evaluateCells", () => {
C1: "=1.2e4",
C2: "=1e5",
C3: "=-1e3",
C4: "=1.2E4",
C5: "=1E5",
C6: "=-1E3",
C7: "=1E309", // too big number

D1: "=1.1.1", // not a number
};
expect(evaluateGrid(grid)).toEqual({
Expand All @@ -90,6 +97,10 @@ describe("evaluateCells", () => {
C1: 12000,
C2: 100000,
C3: -1000,
C4: 12000,
C5: 100000,
C6: -1000,
C7: "#NUM!",
D1: "#BAD_EXPR",
});
});
Expand Down Expand Up @@ -1262,6 +1273,36 @@ describe("evaluateCells", () => {
});
});
});

describe("too big number", () => {
test("in a formula", () => {
const grid = {
A1: "1E200",
A2: "=POWER(A1,2)",
};
expect(evaluateCell("A2", grid)).toBe("#NUM!");
});
test("in a matrix", () => {
const grid = {
A1: "1E200",
B1: "1",
C1: "1E200",
C2: "1",
D1: "=MMULT(A1:B1,C1:C2)",
};
expect(evaluateCell("D1", grid)).toBe("#NUM!");
});
test("in a vectorization", () => {
const grid = {
A1: "1E200",
A2: "1",
B1: "1E200",
B2: "1",
C1: "=(A1:A2)*(B1:B2)",
};
expect(evaluateCell("C1", grid)).toBe("#NUM!");
});
});
});

describe("evaluate formula getter", () => {
Expand Down
3 changes: 3 additions & 0 deletions tests/evaluation/parser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,9 @@ describe("parser", () => {
expect(parse("1e3")).toMatchObject({ type: "NUMBER", value: 1e3 });
expect(parse("1e+3")).toMatchObject({ type: "NUMBER", value: 1e3 });
expect(parse("1e-3")).toMatchObject({ type: "NUMBER", value: 1e-3 });
expect(parse("1E3")).toMatchObject({ type: "NUMBER", value: 1e3 });
expect(parse("1E+3")).toMatchObject({ type: "NUMBER", value: 1e3 });
expect(parse("1E-3")).toMatchObject({ type: "NUMBER", value: 1e-3 });
});

test("can parse string without ending double quotes", () => {
Expand Down
3 changes: 3 additions & 0 deletions tests/evaluation/tokenizer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,9 @@ describe("tokenizer", () => {
expect(tokenize("1e3")).toEqual([{ type: "NUMBER", value: "1e3" }]);
expect(tokenize("1e+3")).toEqual([{ type: "NUMBER", value: "1e+3" }]);
expect(tokenize("1e-3")).toEqual([{ type: "NUMBER", value: "1e-3" }]);
expect(tokenize("1E3")).toEqual([{ type: "NUMBER", value: "1E3" }]);
expect(tokenize("1E+3")).toEqual([{ type: "NUMBER", value: "1E+3" }]);
expect(tokenize("1E-3")).toEqual([{ type: "NUMBER", value: "1E-3" }]);
});

test("debug formula token", () => {
Expand Down
Loading