Skip to content

Commit

Permalink
feat(formula): today function, set numfmt data (#1295)
Browse files Browse the repository at this point in the history
* feat(formula): today function, set numfmt data

* feat(formula): supports numfmt

* test(formula): set numfmt data

* fix(formula): sum get numfmt from link cell

* fix(sheet): recover status bar

* fix(formula): set numfmt on formula cell

* feat(formula): date function single number

* feat(formula): date function with array

* fix(formula): date function test, link to formatted cell supports format

* fix(formula): date serial calculation

* fix(formula): function calculate null params, abs gets 0 when linked to blank cell

* fix(formula): concatenate calculate zero value

* fix(formula): sum,max,min .etc add blank params check

* test(formula): new test template

* fix(formula): date function gets error when date before 1900.1.1

* feat(formula): add edate function

* test(formula): test edate function

* feat(formula): year,month,day functions

* feat(formula): match, xmatch functions

* test(formula): xmatch function test case

* test(formula): nested functions test

* fix(formula): formula cell shows format, cell calculation sets percentage

* fix(formula): edate,month,year functions support blank cell

* fix(formula):  xmatch description
  • Loading branch information
Dushusir committed Feb 20, 2024
1 parent 8ca9cc1 commit f069dd8
Show file tree
Hide file tree
Showing 92 changed files with 3,146 additions and 785 deletions.
35 changes: 35 additions & 0 deletions packages/engine-formula/src/basics/__tests__/date.spec.ts
@@ -0,0 +1,35 @@
/**
* Copyright 2023-present DreamNum Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { describe, expect, it } from 'vitest';
import { excelDateSerial, excelSerialToDate, formatDateDefault } from '../date';

describe('Test date', () => {
it('Function excelDateSerial', () => {
expect(excelDateSerial(new Date(1900, 1, 28))).toBe(59);
expect(excelDateSerial(new Date(1900, 1, 29))).toBe(61);
expect(excelDateSerial(new Date(1900, 2, 1))).toBe(61);
expect(excelDateSerial(new Date(1901, 0, 1))).toBe(367);
expect(excelDateSerial(new Date(2024, 1, 2))).toBe(45324);
});

it('Function excelSerialToDate', () => {
expect(formatDateDefault(excelSerialToDate(59))).toBe('1900/02/28');
expect(formatDateDefault(excelSerialToDate(61))).toBe('1900/03/01');
expect(formatDateDefault(excelSerialToDate(367))).toBe('1901/01/01');
expect(formatDateDefault(excelSerialToDate(45324))).toBe('2024/02/02');
});
});
5 changes: 5 additions & 0 deletions packages/engine-formula/src/basics/common.ts
Expand Up @@ -100,6 +100,10 @@ export interface IOtherFormulaData {
[unitId: string]: Nullable<{ [subUnitId: string]: Nullable<{ [formulaId: string]: IFormulaDataItem }> }>;
}

export interface INumfmtItemMap {
[unitId: string]: Nullable<{ [sheetId: string]: IObjectMatrixPrimitiveType<Nullable<string>> }>;
}

/**
* @f formulaString, the text string of the formula.
* @si The formula ID can be utilized in scenarios such as copy-pasting and drag-filling to convert formulas into references, eliminating the need for recreating the formulaString.
Expand Down Expand Up @@ -139,6 +143,7 @@ export interface IFormulaDatasetConfig {
dirtyRanges: IUnitRange[];
dirtyNameMap: IDirtyUnitSheetNameMap;
dirtyUnitFeatureMap: IDirtyUnitFeatureMap;
numfmtItemMap: INumfmtItemMap;
excludedCell?: IUnitExcludedCell;
allUnitData?: IUnitData;
unitSheetNameMap?: IUnitSheetNameMap;
Expand Down
75 changes: 75 additions & 0 deletions packages/engine-formula/src/basics/date.ts
@@ -0,0 +1,75 @@
/**
* Copyright 2023-present DreamNum Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

export const DEFFAULT_DATE_FORMAT = 'yyyy-mm-dd;@';

/**
* Excel stores dates as sequential serial numbers so they can be used in calculations. By default, January 1, 1900 is serial number 1, and January 1, 2008 is serial number 39448 because it is 39,447 days after January 1, 1900.
*
* Excel has a leap year error in 1900. February 29, 1900 is considered a legal date. In fact, there is no February 29 in 1900.
* 1900.2.28 Date Serial 59
* 1900.2.29 Date Serial 61
* 1900.3.1 Date Serial 61
* 1901.1.1 Date Serial 367
* @param date
* @returns
*/
export function excelDateSerial(date: Date): number {
const baseDate = new Date(Date.UTC(1900, 0, 1)); // January 1, 1900, UTC
const leapDayDate = new Date(Date.UTC(1900, 1, 28)); // February 28, 1900, UTC
const dateInUTC = Date.UTC(date.getFullYear(), date.getMonth(), date.getDate());

// Calculate the difference in days between the base date and the input date
let dayDifference = (dateInUTC - baseDate.getTime()) / (1000 * 3600 * 24);

// If the date is later than February 28, 1900, the day difference needs to be adjusted to account for Excel errors
if (dateInUTC > leapDayDate.getTime()) {
dayDifference += 1;
}

return Math.floor(dayDifference) + 1; // Excel serial number starts from 1
}

export function excelSerialToDate(serial: number): Date {
const baseDate = new Date(Date.UTC(1900, 0, 1)); // January 1, 1900, UTC
const leapDayDate = new Date(Date.UTC(1900, 1, 28)); // February 28, 1900, UTC

let dayDifference = Math.floor(serial) - 1; // Adjust for Excel serial number starting from 1

// If the serial number corresponds to a date later than February 28, 1900, adjust the day difference
if (dayDifference > (leapDayDate.getTime() - baseDate.getTime()) / (1000 * 3600 * 24)) {
dayDifference -= 1;
}

const resultDate = new Date(baseDate.getTime() + dayDifference * (1000 * 3600 * 24));
return resultDate;
}

export function formatDateDefault(date: Date): string {
// Get the year from the date object
const year: number = date.getFullYear();

// Get the month from the date object and add 1 (since getMonth() returns 0-11)
// Convert it to a string and pad with zero if necessary to ensure two digits
const month: string = (date.getMonth() + 1).toString().padStart(2, '0');

// Get the day from the date object
// Convert it to a string and pad with zero if necessary to ensure two digits
const day: string = date.getDate().toString().padStart(2, '0');

// Concatenate year, month, and day with '/' as separator to form yyyy/mm/dd format
return `${year}/${month}/${day}`;
}
10 changes: 10 additions & 0 deletions packages/engine-formula/src/basics/object-class-type.ts
Expand Up @@ -17,6 +17,16 @@
import { Disposable } from '@univerjs/core';

export class ObjectClassType extends Disposable {
pattern: string = '';

getPattern() {
return this.pattern;
}

setPattern(pattern: string) {
this.pattern = pattern;
}

isError() {
return false;
}
Expand Down
Expand Up @@ -20,6 +20,7 @@ import { CommandType } from '@univerjs/core';
import type {
IDirtyUnitFeatureMap,
IDirtyUnitSheetNameMap,
INumfmtItemMap,
IRuntimeOtherUnitDataType,
IRuntimeUnitDataType,
} from '../../basics/common';
Expand All @@ -30,6 +31,7 @@ export interface ISetFormulaCalculationStartMutation {
dirtyNameMap: IDirtyUnitSheetNameMap;
dirtyUnitFeatureMap: IDirtyUnitFeatureMap;
options: Nullable<IExecutionOptions>;
numfmtItemMap: INumfmtItemMap;
forceCalculation?: boolean;
}
/**
Expand Down
@@ -0,0 +1,36 @@
/**
* Copyright 2023-present DreamNum Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import type { IMutation } from '@univerjs/core';
import { CommandType } from '@univerjs/core';
import type { IAccessor } from '@wendellhu/redi';

import type { INumfmtItemMap } from '../../basics/common';
import { FormulaDataModel } from '../../models/formula-data.model';

export interface ISetNumfmtFormulaDataMutationParams {
numfmtItemMap: INumfmtItemMap;
}

export const SetNumfmtFormulaDataMutation: IMutation<ISetNumfmtFormulaDataMutationParams> = {
id: 'formula.mutation.set-numfmt-formula-data',
type: CommandType.MUTATION,
handler: (accessor: IAccessor, params: ISetNumfmtFormulaDataMutationParams) => {
const formulaDataModel = accessor.get(FormulaDataModel);
formulaDataModel.updateNumfmtItemMap(params.numfmtItemMap);
return true;
},
};
28 changes: 22 additions & 6 deletions packages/engine-formula/src/controller/calculate.controller.ts
Expand Up @@ -15,10 +15,10 @@
*/

import type { ICommandInfo, IUnitRange } from '@univerjs/core';
import { Disposable, ICommandService, IUniverInstanceService, LifecycleStages, OnLifecycle } from '@univerjs/core';
import { Disposable, ICommandService, IUniverInstanceService, LifecycleStages, OnLifecycle, Tools } from '@univerjs/core';
import { Inject } from '@wendellhu/redi';

import type { IDirtyUnitFeatureMap, IDirtyUnitSheetNameMap, IFormulaData } from '../basics/common';
import type { IDirtyUnitFeatureMap, IDirtyUnitSheetNameMap, IFormulaData, INumfmtItemMap } from '../basics/common';
import type { ISetArrayFormulaDataMutationParams } from '../commands/mutations/set-array-formula-data.mutation';
import { SetArrayFormulaDataMutation } from '../commands/mutations/set-array-formula-data.mutation';
import type { ISetFormulaCalculationStartMutation } from '../commands/mutations/set-formula-calculation.mutation';
Expand All @@ -34,6 +34,7 @@ import { FormulaDataModel } from '../models/formula-data.model';
import { CalculateFormulaService } from '../services/calculate-formula.service';
import type { IAllRuntimeData } from '../services/runtime.service';
import { FormulaExecutedStateType } from '../services/runtime.service';
import { SetNumfmtFormulaDataMutation } from '../commands/mutations/set-numfmt-formula-data.mutation';

@OnLifecycle(LifecycleStages.Ready, CalculateController)
export class CalculateController extends Disposable {
Expand Down Expand Up @@ -69,9 +70,9 @@ export class CalculateController extends Disposable {
if (params.forceCalculation === true) {
this._calculate(true);
} else {
const { dirtyRanges, dirtyNameMap, dirtyUnitFeatureMap } = params;
const { dirtyRanges, dirtyNameMap, dirtyUnitFeatureMap, numfmtItemMap } = params;

this._calculate(false, dirtyRanges, dirtyNameMap, dirtyUnitFeatureMap);
this._calculate(false, dirtyRanges, dirtyNameMap, dirtyUnitFeatureMap, numfmtItemMap);
}
} else if (command.id === SetArrayFormulaDataMutation.id) {
const params = command.params as ISetArrayFormulaDataMutationParams;
Expand All @@ -92,7 +93,8 @@ export class CalculateController extends Disposable {
forceCalculate: boolean = false,
dirtyRanges: IUnitRange[] = [],
dirtyNameMap: IDirtyUnitSheetNameMap = {},
dirtyUnitFeatureMap: IDirtyUnitFeatureMap = {}
dirtyUnitFeatureMap: IDirtyUnitFeatureMap = {},
numfmtItemMap: INumfmtItemMap = {}
) {
if (
dirtyRanges.length === 0 &&
Expand All @@ -116,6 +118,7 @@ export class CalculateController extends Disposable {
dirtyRanges,
dirtyNameMap,
dirtyUnitFeatureMap,
numfmtItemMap,
});
}

Expand Down Expand Up @@ -186,7 +189,7 @@ export class CalculateController extends Disposable {
}

private async _applyFormula(data: IAllRuntimeData) {
const { unitData, unitOtherData, arrayFormulaRange, arrayFormulaCellData, clearArrayFormulaCellData } = data;
const { unitData, unitOtherData, arrayFormulaRange, arrayFormulaCellData, clearArrayFormulaCellData, numfmtItemMap } = data;

if (!unitData) {
console.error('No sheetData from Formula Engine!');
Expand Down Expand Up @@ -215,6 +218,19 @@ export class CalculateController extends Disposable {
);
}

// Synchronous to the main thread
if (!Tools.isEmptyObject(numfmtItemMap)) {
this._commandService.executeCommand(
SetNumfmtFormulaDataMutation.id,
{
numfmtItemMap,
},
{
onlyLocal: true,
}
);
}

this._commandService.executeCommand(
SetFormulaCalculationResultMutation.id,
{
Expand Down
2 changes: 2 additions & 0 deletions packages/engine-formula/src/controller/formula.controller.ts
Expand Up @@ -58,6 +58,7 @@ import { functionText } from '../functions/text/function-map';
import { functionUniver } from '../functions/univer/function-map';
import { functionWeb } from '../functions/web/function-map';
import { IFunctionService } from '../services/function.service';
import { SetNumfmtFormulaDataMutation } from '../commands/mutations/set-numfmt-formula-data.mutation';

@OnLifecycle(LifecycleStages.Ready, FormulaController)
export class FormulaController extends Disposable {
Expand Down Expand Up @@ -85,6 +86,7 @@ export class FormulaController extends Disposable {
SetFormulaCalculationStopMutation,
SetFormulaCalculationNotificationMutation,
SetFormulaCalculationResultMutation,
SetNumfmtFormulaDataMutation,

SetDefinedNameMutation,
RemoveDefinedNameMutation,
Expand Down
Expand Up @@ -59,6 +59,7 @@ describe('Test indirect', () => {
formulaData: {},
arrayFormulaCellData: {},
forceCalculate: false,
numfmtItemMap: {},
dirtyRanges: [],
dirtyNameMap: {},
dirtyUnitFeatureMap: {},
Expand Down
2 changes: 2 additions & 0 deletions packages/engine-formula/src/engine/ast-node/reference-node.ts
Expand Up @@ -71,6 +71,8 @@ export class ReferenceNode extends BaseAstNode {

this._referenceObject.setRuntimeData(runtimeService.getUnitData());

this._referenceObject.setNumfmtItemData(currentConfigService.getNumfmtItemMap());

this._referenceObject.setRuntimeArrayFormulaCellData(runtimeService.getRuntimeArrayFormulaCellData());

this._referenceObject.setRuntimeFeatureCellData(runtimeService.getRuntimeFeatureCellData());
Expand Down
3 changes: 3 additions & 0 deletions packages/engine-formula/src/engine/ast-node/suffix-node.ts
Expand Up @@ -63,6 +63,9 @@ export class SuffixNode extends BaseAstNode {
value as BaseValueObject,
new NumberValueObject(100)
) as FunctionVariantType;

// set number format
result.setPattern('0.00%');
} else if (this._operatorString === suffixToken.POUND) {
result = this._handlerPound(value);
} else {
Expand Down
Expand Up @@ -18,7 +18,7 @@ import type { ICellData, IRange, Nullable } from '@univerjs/core';
import { CellValueType, isNullCell } from '@univerjs/core';

import { FormulaAstLRU } from '../../basics/cache-lru';
import type { IRuntimeUnitDataType, IUnitData, IUnitSheetNameMap } from '../../basics/common';
import type { INumfmtItemMap, IRuntimeUnitDataType, IUnitData, IUnitSheetNameMap } from '../../basics/common';
import { ERROR_TYPE_SET, ErrorType } from '../../basics/error-type';
import { ObjectClassType } from '../../basics/object-class-type';
import { ArrayValueObject, ValueObjectFactory } from '../value-object/array-value-object';
Expand Down Expand Up @@ -66,6 +66,8 @@ export class BaseReferenceObject extends ObjectClassType {

private _runtimeFeatureCellData: { [featureId: string]: IRuntimeUnitDataType } = {};

private _numfmtItemData: INumfmtItemMap = {};

private _refOffsetX = 0;

private _refOffsetY = 0;
Expand Down Expand Up @@ -144,6 +146,9 @@ export class BaseReferenceObject extends ObjectClassType {
return callback(new ErrorValueObject(ErrorType.VALUE), startRow, startColumn);
}

const unitId = this._forcedUnitId || this._defaultUnitId;
const sheetId = this._forcedSheetId || this._defaultSheetId;

for (let r = startRow; r <= endRow; r++) {
for (let c = startColumn; c <= endColumn; c++) {
if (r < 0 || c < 0) {
Expand All @@ -159,6 +164,9 @@ export class BaseReferenceObject extends ObjectClassType {

const resultObjectValue = this.getCellValueObject(cell);

const pattern = this._numfmtItemData[unitId]?.[sheetId]?.[r]?.[c];
pattern && resultObjectValue.setPattern(pattern);

result = callback(resultObjectValue, r, c);

if (result === false) {
Expand All @@ -176,7 +184,15 @@ export class BaseReferenceObject extends ObjectClassType {
return new NumberValueObject(0, true);
}

return this.getCellValueObject(cell);
const cellValueObject = this.getCellValueObject(cell);

// Set numfmt pattern
const unitId = this._forcedUnitId || this._defaultUnitId;
const sheetId = this._forcedSheetId || this._defaultSheetId;
const numfmtItem = this._numfmtItemData[unitId]?.[sheetId]?.[startRow]?.[startColumn];
numfmtItem && cellValueObject.setPattern(numfmtItem);

return cellValueObject;
}

getRangeData() {
Expand Down Expand Up @@ -289,6 +305,14 @@ export class BaseReferenceObject extends ObjectClassType {
this._runtimeFeatureCellData = unitData;
}

getNumfmtItemData() {
return this._numfmtItemData;
}

setNumfmtItemData(numfmtItemData: INumfmtItemMap) {
this._numfmtItemData = numfmtItemData;
}

getRowCount() {
return this.getCurrentActiveSheetData().rowCount;
}
Expand Down

0 comments on commit f069dd8

Please sign in to comment.