snapshot of https://github.com/dream-num/univer.git (dev, 2026-09-16; blobs > 131072B omitted)
Some checks failed
📦 Build / build (push) Has been cancelled
🧪 CI Test / test shard (1/4) (push) Has been cancelled
🧪 CI Test / test shard (2/4) (push) Has been cancelled
🧪 CI Test / test shard (3/4) (push) Has been cancelled
🧪 CI Test / test shard (4/4) (push) Has been cancelled
🧪 CI Test / test (push) Has been cancelled
🧪 CI Test / eslint (push) Has been cancelled
🧪 CI Test / typecheck (push) Has been cancelled
🔫 Sync to Pro / dispatch-sync (push) Has been cancelled
🔮 Sync Mirror / sync (push) Has been cancelled
Some checks failed
📦 Build / build (push) Has been cancelled
🧪 CI Test / test shard (1/4) (push) Has been cancelled
🧪 CI Test / test shard (2/4) (push) Has been cancelled
🧪 CI Test / test shard (3/4) (push) Has been cancelled
🧪 CI Test / test shard (4/4) (push) Has been cancelled
🧪 CI Test / test (push) Has been cancelled
🧪 CI Test / eslint (push) Has been cancelled
🧪 CI Test / typecheck (push) Has been cancelled
🔫 Sync to Pro / dispatch-sync (push) Has been cancelled
🔮 Sync Mirror / sync (push) Has been cancelled
This commit is contained in:
31
packages/sheets-numfmt/src/__tests__/date.test.ts
Normal file
31
packages/sheets-numfmt/src/__tests__/date.test.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* 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 { numfmt } from '@univerjs/core';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
describe('test numfmt date', () => {
|
||||
it('date parse date', () => {
|
||||
expect(numfmt.parseDate('asdasdasd')).toBe(null);
|
||||
expect(numfmt.parseDate('1988/12/12 21:21:21')?.z).toBe('yyyy/mm/dd hh:mm:ss');
|
||||
expect(numfmt.parseDate('12/12 21:21:21')?.z).toBe('mm/dd hh:mm:ss');
|
||||
expect(numfmt.parseDate('2012/12 21:21:21')?.z).toBe('yyyy/m hh:mm:ss'); // TODO: is yyy/mm hh:mm:ss ?
|
||||
expect(numfmt.parseDate('2012/12 21:21')?.z).toBe('yyyy/m hh:mm'); // TODO: is yyy/mm hh:mm ?
|
||||
expect(numfmt.parseDate('2012/12')?.z).toBe('yyyy/m'); // TODO: is yyy/mm ?
|
||||
expect(numfmt.parseDate('2012/12/12')?.z).toBe('yyyy/mm/dd');
|
||||
expect(numfmt.parseDate('21:21')).toBe(null);
|
||||
});
|
||||
});
|
||||
304
packages/sheets-numfmt/src/__tests__/utils.test.ts
Normal file
304
packages/sheets-numfmt/src/__tests__/utils.test.ts
Normal file
@@ -0,0 +1,304 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* 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 { cellToRange, CellValueType, currencySymbols, DEFAULT_NUMBER_FORMAT, DEFAULT_TEXT_FORMAT_EXCEL, isPatternEqualWithoutDecimal, LocaleType } from '@univerjs/core';
|
||||
import { RemoveNumfmtMutation, SetNumfmtMutation } from '@univerjs/sheets';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import { getCurrencyFormat, getCurrencySymbolByLocale, getCurrencySymbolIconByLocale } from '../base/const/currency-symbols';
|
||||
import { getCurrencyType } from '../utils/currency';
|
||||
import { getDecimalFromPattern, getDecimalString, isPatternHasDecimal, setPatternDecimal } from '../utils/decimal';
|
||||
import { mergeNumfmtMutations } from '../utils/mutation';
|
||||
import { getCurrencyFormatOptions, getCurrencyOptions, getDateFormatOptions, getNumberFormatOptions } from '../utils/options';
|
||||
import { getPatternPreview, getPatternPreviewIgnoreGeneral, getPatternType } from '../utils/pattern';
|
||||
import { getScientificNotationFormatFromCell, isAllowedPatternForScientificNotationNumber, isScientificNotationNumericCell } from '../utils/scientific-notation';
|
||||
|
||||
describe('test numfmt utils function', () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('getCurrencyType', () => {
|
||||
expect(getCurrencyType(`_(${currencySymbols[7]} 123123`)).toBe(currencySymbols[7]);
|
||||
expect(getCurrencyType('_(# 123123')).toBeUndefined();
|
||||
expect(getCurrencyType(`_(${currencySymbols[7]} 123123 ${currencySymbols[8]}`)).toBe(currencySymbols[7]);
|
||||
});
|
||||
it('getDecimalFromPattern', () => {
|
||||
expect(getDecimalFromPattern('_(###0.000);--###0.00')).toBe(3);
|
||||
expect(getDecimalFromPattern('_(###0);-###0.00')).toBe(0); // the function decimal just use positive,negative configuration ignored
|
||||
expect(getDecimalFromPattern('_(###0.0);--###0.00')).toBe(1);
|
||||
expect(getDecimalFromPattern('_(###0.);--###0.00')).toBe(0);
|
||||
});
|
||||
it('isPatternEqualWithoutDecimal', () => {
|
||||
expect(isPatternEqualWithoutDecimal('_(###0.000);-###0.00', '[red]_(###0.0000);-###0.00')).toBe(true); // the positive color ignored
|
||||
expect(isPatternEqualWithoutDecimal('_(###0;-###0.00', '_(###0;[red]-###0.00')).toBe(false);
|
||||
expect(isPatternEqualWithoutDecimal('_(###00.0;-###0.00', '_(###0;[red]-###0.00')).toBe(false);
|
||||
expect(isPatternEqualWithoutDecimal('_(###0;-###0.00', '_(###0.00;-###0.00')).toBe(true);
|
||||
});
|
||||
it('getDecimalString', () => {
|
||||
expect(getDecimalString(3)).toBe('000'); // the positive color ignored
|
||||
expect(getDecimalString(-1)).toBe(''); // the positive color ignored
|
||||
expect(getDecimalString(0)).toBe(''); // the positive color ignored
|
||||
});
|
||||
it('setPatternDecimal', () => {
|
||||
expect(setPatternDecimal('0.', 4)).toBe('0.0000'); // the positive color ignored
|
||||
expect(setPatternDecimal('.', 4)).toBe('.0000'); // the positive color ignored
|
||||
expect(setPatternDecimal('0.0', 4)).toBe('0.0000'); // the positive color ignored
|
||||
expect(setPatternDecimal('0.0', 0)).toBe('0'); // the positive color ignored
|
||||
});
|
||||
|
||||
it('isPatternHasDecimal', () => {
|
||||
expect(isPatternHasDecimal('0.00')).toBe(true);
|
||||
expect(isPatternHasDecimal('#,##0')).toBe(true);
|
||||
expect(isPatternHasDecimal('General')).toBe(false);
|
||||
});
|
||||
|
||||
it('currency symbol helpers', () => {
|
||||
expect(getCurrencySymbolByLocale(LocaleType.ZH_CN)).toBe('¥');
|
||||
expect(getCurrencySymbolByLocale(LocaleType.KO_KR)).toBe('₩');
|
||||
expect(getCurrencySymbolIconByLocale(LocaleType.FR_FR)).toEqual({
|
||||
icon: 'EuroIcon',
|
||||
symbol: '€',
|
||||
locale: LocaleType.FR_FR,
|
||||
});
|
||||
expect(getCurrencySymbolIconByLocale(LocaleType.KO_KR)).toEqual({
|
||||
icon: 'WonIcon',
|
||||
symbol: '₩',
|
||||
locale: LocaleType.KO_KR,
|
||||
});
|
||||
expect([
|
||||
LocaleType.VI_VN,
|
||||
LocaleType.FA_IR,
|
||||
LocaleType.AR_SA,
|
||||
LocaleType.ID_ID,
|
||||
LocaleType.PL_PL,
|
||||
].map((locale) => getCurrencySymbolIconByLocale(locale).icon)).toEqual([
|
||||
'DongIcon',
|
||||
'RialIcon',
|
||||
'RialIcon',
|
||||
'RupiahIcon',
|
||||
'ZlotyIcon',
|
||||
]);
|
||||
expect(getCurrencyFormat(LocaleType.ZH_CN, 3)).toContain('"¥"#,##0.000');
|
||||
expect(getCurrencyFormat(LocaleType.EN_US, 200)).toContain(`.${'0'.repeat(127)}`);
|
||||
});
|
||||
|
||||
it('option helpers expose formatted labels and values', () => {
|
||||
expect(getCurrencyOptions()[0]).toEqual({ label: 'Rp', value: 'Rp' });
|
||||
expect(getCurrencyFormatOptions('€')[0]).toMatchObject({
|
||||
label: expect.stringContaining('€'),
|
||||
value: expect.stringContaining('€'),
|
||||
});
|
||||
expect(getDateFormatOptions()[0]).toMatchObject({
|
||||
label: expect.any(String),
|
||||
value: expect.any(String),
|
||||
});
|
||||
expect(getNumberFormatOptions()[0]).toMatchObject({
|
||||
label: expect.any(String),
|
||||
value: expect.any(String),
|
||||
});
|
||||
});
|
||||
|
||||
it('pattern preview helpers return types and gracefully handle general or invalid formats', () => {
|
||||
vi.spyOn(console, 'warn').mockImplementation(() => undefined);
|
||||
|
||||
expect(getPatternType('0%')).toBe('percent');
|
||||
expect(getPatternPreview('0.00', 12.3)).toEqual({ result: '12.30' });
|
||||
expect(getPatternPreviewIgnoreGeneral(DEFAULT_NUMBER_FORMAT, 0.1 + 0.2)).toEqual({ result: '0.3' });
|
||||
expect(getPatternPreview('[invalid', 42)).toEqual({ result: '42' });
|
||||
});
|
||||
|
||||
it('scientific notation helpers detect numeric cells and derive display patterns from mantissa decimals', () => {
|
||||
expect(isScientificNotationNumericCell({ v: 1e21, t: CellValueType.NUMBER })).toBe(true);
|
||||
expect(isScientificNotationNumericCell({ v: 1.2345e31, t: CellValueType.NUMBER })).toBe(true);
|
||||
expect(isScientificNotationNumericCell({ v: '1e21', t: CellValueType.STRING })).toBe(false);
|
||||
expect(isScientificNotationNumericCell({ v: 123, t: CellValueType.NUMBER })).toBe(false);
|
||||
expect(isScientificNotationNumericCell(null)).toBe(false);
|
||||
|
||||
expect(getScientificNotationFormatFromCell({ v: 1e21, t: CellValueType.NUMBER })).toBe('0.00E+00');
|
||||
expect(getScientificNotationFormatFromCell({ v: 1.2e21, t: CellValueType.NUMBER })).toBe('0.00E+00');
|
||||
expect(getScientificNotationFormatFromCell({ v: 1.2345e31, t: CellValueType.NUMBER })).toBe('0.0000E+00');
|
||||
expect(getScientificNotationFormatFromCell({ v: -1.2345e-31, t: CellValueType.NUMBER })).toBe('0.0000E+00');
|
||||
});
|
||||
|
||||
it('scientific notation format guard only allows general, text, and scientific patterns', () => {
|
||||
expect(isAllowedPatternForScientificNotationNumber(undefined)).toBe(true);
|
||||
expect(isAllowedPatternForScientificNotationNumber(DEFAULT_NUMBER_FORMAT)).toBe(true);
|
||||
expect(isAllowedPatternForScientificNotationNumber(DEFAULT_TEXT_FORMAT_EXCEL)).toBe(true);
|
||||
expect(isAllowedPatternForScientificNotationNumber('0.00E+00')).toBe(true);
|
||||
expect(isAllowedPatternForScientificNotationNumber('0.00')).toBe(false);
|
||||
expect(isAllowedPatternForScientificNotationNumber('0%')).toBe(false);
|
||||
});
|
||||
|
||||
it('mergeNumfmtMutations', () => {
|
||||
const result = mergeNumfmtMutations([
|
||||
{
|
||||
id: SetNumfmtMutation.id,
|
||||
params: {
|
||||
values: {
|
||||
a: { ranges: [cellToRange(2, 3), cellToRange(2, 4), cellToRange(4, 5)] },
|
||||
},
|
||||
refMap: { a: { pattern: '', type: '' } },
|
||||
},
|
||||
},
|
||||
{
|
||||
id: RemoveNumfmtMutation.id,
|
||||
params: {
|
||||
ranges: [cellToRange(2, 3), cellToRange(2, 4), cellToRange(4, 5)],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: SetNumfmtMutation.id,
|
||||
params: {
|
||||
values: {
|
||||
a: { ranges: [cellToRange(4, 6), cellToRange(4, 5)] },
|
||||
},
|
||||
refMap: { a: { pattern: '', type: '' } },
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const removeMutation = result[0] as { params: { ranges: unknown } };
|
||||
const setMutation = result[1] as { params: { values: Record<string, { ranges: unknown }> } };
|
||||
|
||||
expect(result[0].id === RemoveNumfmtMutation.id).toBeTruthy();
|
||||
expect(result[1].id === SetNumfmtMutation.id).toBeTruthy();
|
||||
expect(removeMutation.params.ranges).toEqual([
|
||||
{
|
||||
startRow: 2,
|
||||
endRow: 2,
|
||||
startColumn: 3,
|
||||
endColumn: 4,
|
||||
},
|
||||
{
|
||||
startRow: 4,
|
||||
endRow: 4,
|
||||
startColumn: 5,
|
||||
endColumn: 5,
|
||||
},
|
||||
]);
|
||||
expect(setMutation.params.values['1'].ranges).toEqual([
|
||||
{
|
||||
startRow: 2,
|
||||
endRow: 2,
|
||||
startColumn: 3,
|
||||
endColumn: 4,
|
||||
},
|
||||
{
|
||||
startRow: 4,
|
||||
endRow: 4,
|
||||
startColumn: 5,
|
||||
endColumn: 6,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('mergeNumfmtMutations multiple data formats of different types', () => {
|
||||
const result = mergeNumfmtMutations([
|
||||
{
|
||||
id: SetNumfmtMutation.id,
|
||||
params: {
|
||||
values: {
|
||||
a: { ranges: [cellToRange(2, 3), cellToRange(2, 4), cellToRange(4, 5)] },
|
||||
},
|
||||
refMap: { a: { pattern: 'a', type: '' } },
|
||||
},
|
||||
},
|
||||
{
|
||||
id: RemoveNumfmtMutation.id,
|
||||
params: {
|
||||
ranges: [cellToRange(2, 3), cellToRange(2, 4), cellToRange(4, 5)],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: SetNumfmtMutation.id,
|
||||
params: {
|
||||
values: {
|
||||
a: { ranges: [cellToRange(4, 6), cellToRange(4, 5)] },
|
||||
},
|
||||
refMap: { a: { pattern: 'b', type: '' } },
|
||||
},
|
||||
},
|
||||
]);
|
||||
expect(result).toEqual([
|
||||
{
|
||||
id: 'sheet.mutation.remove.numfmt',
|
||||
params: {
|
||||
ranges: [
|
||||
{
|
||||
startRow: 2,
|
||||
endRow: 2,
|
||||
startColumn: 3,
|
||||
endColumn: 4,
|
||||
},
|
||||
{
|
||||
startRow: 4,
|
||||
endRow: 4,
|
||||
startColumn: 5,
|
||||
endColumn: 5,
|
||||
},
|
||||
],
|
||||
unitId: undefined,
|
||||
subUnitId: undefined,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'sheet.mutation.set.numfmt',
|
||||
params: {
|
||||
values: {
|
||||
1: {
|
||||
ranges: [
|
||||
{
|
||||
startRow: 2,
|
||||
endRow: 2,
|
||||
startColumn: 3,
|
||||
endColumn: 4,
|
||||
},
|
||||
{
|
||||
startRow: 4,
|
||||
endRow: 4,
|
||||
startColumn: 5,
|
||||
endColumn: 5,
|
||||
},
|
||||
],
|
||||
},
|
||||
2: {
|
||||
ranges: [
|
||||
{
|
||||
startRow: 4,
|
||||
endRow: 4,
|
||||
startColumn: 5,
|
||||
endColumn: 6,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
refMap: {
|
||||
1: {
|
||||
pattern: 'a',
|
||||
type: '',
|
||||
},
|
||||
2: {
|
||||
pattern: 'b',
|
||||
type: '',
|
||||
},
|
||||
},
|
||||
unitId: undefined,
|
||||
subUnitId: undefined,
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
138
packages/sheets-numfmt/src/base/const/currency-symbols.ts
Normal file
138
packages/sheets-numfmt/src/base/const/currency-symbols.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* 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 { LocaleType } from '@univerjs/core';
|
||||
|
||||
export const localeCurrencySymbolMap = new Map<LocaleType, string>([
|
||||
[LocaleType.EN_US, '$'],
|
||||
[LocaleType.RU_RU, '₽'],
|
||||
[LocaleType.VI_VN, '₫'],
|
||||
[LocaleType.ZH_CN, '¥'],
|
||||
[LocaleType.ZH_TW, 'NT$'],
|
||||
[LocaleType.ZH_HK, 'HK$'],
|
||||
[LocaleType.FR_FR, '€'],
|
||||
[LocaleType.FA_IR, '﷼'],
|
||||
[LocaleType.KO_KR, '₩'],
|
||||
[LocaleType.ES_ES, '€'],
|
||||
[LocaleType.CA_ES, '€'],
|
||||
[LocaleType.SK_SK, '€'],
|
||||
[LocaleType.JA_JP, '¥'],
|
||||
[LocaleType.PT_BR, 'R$'],
|
||||
[LocaleType.DE_DE, '€'],
|
||||
[LocaleType.IT_IT, '€'],
|
||||
[LocaleType.ID_ID, 'Rp'],
|
||||
[LocaleType.PL_PL, 'zł'],
|
||||
[LocaleType.AR_SA, '﷼'],
|
||||
]);
|
||||
|
||||
/**
|
||||
* Get the currency symbol icon based on the locale.
|
||||
*/
|
||||
export function getCurrencySymbolIconByLocale(locale: LocaleType) {
|
||||
switch (locale) {
|
||||
case LocaleType.CA_ES:
|
||||
case LocaleType.DE_DE:
|
||||
case LocaleType.ES_ES:
|
||||
case LocaleType.FR_FR:
|
||||
case LocaleType.IT_IT:
|
||||
case LocaleType.SK_SK:
|
||||
return {
|
||||
icon: 'EuroIcon',
|
||||
symbol: localeCurrencySymbolMap.get(locale) || '€',
|
||||
locale,
|
||||
};
|
||||
case LocaleType.RU_RU:
|
||||
return {
|
||||
icon: 'RoubleIcon',
|
||||
symbol: localeCurrencySymbolMap.get(locale) || '₽',
|
||||
locale,
|
||||
};
|
||||
case LocaleType.JA_JP:
|
||||
case LocaleType.ZH_CN:
|
||||
return {
|
||||
icon: 'RmbIcon',
|
||||
symbol: localeCurrencySymbolMap.get(locale) || '¥',
|
||||
locale,
|
||||
};
|
||||
case LocaleType.VI_VN:
|
||||
return {
|
||||
icon: 'DongIcon',
|
||||
symbol: localeCurrencySymbolMap.get(locale) || '₫',
|
||||
locale,
|
||||
};
|
||||
case LocaleType.AR_SA:
|
||||
case LocaleType.FA_IR:
|
||||
return {
|
||||
icon: 'RialIcon',
|
||||
symbol: localeCurrencySymbolMap.get(locale) || '﷼',
|
||||
locale,
|
||||
};
|
||||
case LocaleType.KO_KR:
|
||||
return {
|
||||
icon: 'WonIcon',
|
||||
symbol: localeCurrencySymbolMap.get(locale) || '₩',
|
||||
locale,
|
||||
};
|
||||
case LocaleType.ID_ID:
|
||||
return {
|
||||
icon: 'RupiahIcon',
|
||||
symbol: localeCurrencySymbolMap.get(locale) || 'Rp',
|
||||
locale,
|
||||
};
|
||||
case LocaleType.PL_PL:
|
||||
return {
|
||||
icon: 'ZlotyIcon',
|
||||
symbol: localeCurrencySymbolMap.get(locale) || 'zł',
|
||||
locale,
|
||||
};
|
||||
case LocaleType.EN_US:
|
||||
case LocaleType.PT_BR:
|
||||
case LocaleType.ZH_HK:
|
||||
case LocaleType.ZH_TW:
|
||||
default:
|
||||
return {
|
||||
icon: 'DollarIcon',
|
||||
symbol: localeCurrencySymbolMap.get(locale) || '$',
|
||||
locale,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the currency symbol by locale.
|
||||
*/
|
||||
export function getCurrencySymbolByLocale(locale: LocaleType): string {
|
||||
return localeCurrencySymbolMap.get(locale) || '$';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the currency format string based on the locale and number of digits.
|
||||
*/
|
||||
export function getCurrencyFormat(locale: LocaleType, numberDigits: number = 2): string {
|
||||
let _numberDigits = numberDigits;
|
||||
|
||||
if (numberDigits > 127) {
|
||||
_numberDigits = 127;
|
||||
}
|
||||
|
||||
let decimal = '';
|
||||
|
||||
if (_numberDigits > 0) {
|
||||
decimal = `.${'0'.repeat(_numberDigits)}`;
|
||||
}
|
||||
|
||||
return `"${getCurrencySymbolByLocale(locale)}"#,##0${decimal}_);[Red]("${getCurrencySymbolByLocale(locale)}"#,##0${decimal})`;
|
||||
}
|
||||
121
packages/sheets-numfmt/src/base/const/formatdetail.ts
Normal file
121
packages/sheets-numfmt/src/base/const/formatdetail.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* 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 DATEFMTLISG = [
|
||||
{
|
||||
label: '1930-08-05',
|
||||
suffix: 'yyyy-MM-dd',
|
||||
},
|
||||
{
|
||||
label: '1930/08/05',
|
||||
suffix: 'yyyy/MM/dd',
|
||||
},
|
||||
{
|
||||
label: '1930年08月05日',
|
||||
suffix: 'yyyy"年"MM"月"dd"日"',
|
||||
},
|
||||
{
|
||||
label: '08-05',
|
||||
suffix: 'MM-dd',
|
||||
},
|
||||
{
|
||||
label: '8月5日',
|
||||
suffix: 'M"月"d"日"',
|
||||
},
|
||||
{
|
||||
label: '13:30:30',
|
||||
suffix: 'h:mm:ss',
|
||||
},
|
||||
{
|
||||
label: '13:30',
|
||||
suffix: 'h:mm',
|
||||
},
|
||||
{
|
||||
label: '下午01:30',
|
||||
suffix: 'A/P hh:mm',
|
||||
},
|
||||
{
|
||||
label: '下午1:30',
|
||||
suffix: 'A/P h:mm',
|
||||
},
|
||||
{
|
||||
label: '下午1:30:30',
|
||||
suffix: 'A/P h:mm:ss',
|
||||
},
|
||||
{
|
||||
label: '08-05 下午 01:30',
|
||||
suffix: 'MM-dd A/P hh:mm',
|
||||
},
|
||||
];
|
||||
|
||||
export const NUMBERFORMAT = [
|
||||
{
|
||||
label: '(1,235)',
|
||||
suffix: '#,##0_);(#,##0)',
|
||||
},
|
||||
{
|
||||
label: '(1,235) ',
|
||||
suffix: '#,##0_);[Red](#,##0)',
|
||||
color: 'red',
|
||||
},
|
||||
{
|
||||
label: '1,234.56',
|
||||
suffix: '#,##0.00_);#,##0.00',
|
||||
},
|
||||
{
|
||||
label: '1,234.56',
|
||||
suffix: '#,##0.00_);[Red]#,##0.00',
|
||||
color: 'red',
|
||||
},
|
||||
{
|
||||
label: '-1,234.56',
|
||||
suffix: '#,##0.00_);-#,##0.00',
|
||||
},
|
||||
{
|
||||
label: '-1,234.56',
|
||||
suffix: '#,##0.00_);[Red]-#,##0.00',
|
||||
color: 'red',
|
||||
},
|
||||
];
|
||||
export const CURRENCYFORMAT = [
|
||||
{
|
||||
label: (suffix: string) => `${suffix}1,235`,
|
||||
suffix: (suffix: string) => `"${suffix}"#,##0.00_);"${suffix}"#,##0.00`,
|
||||
},
|
||||
{
|
||||
label: (suffix: string) => `${suffix}1,235`,
|
||||
suffix: (suffix: string) => `"${suffix}"#,##0.00_);[Red]"${suffix}"#,##0.00`,
|
||||
color: 'red',
|
||||
},
|
||||
{
|
||||
label: (suffix: string) => `(${suffix}1,235)`,
|
||||
suffix: (suffix: string) => `"${suffix}"#,##0.00_);("${suffix}"#,##0.00)`,
|
||||
},
|
||||
{
|
||||
label: (suffix: string) => `(${suffix}1,235)`,
|
||||
suffix: (suffix: string) => `"${suffix}"#,##0.00_);[Red]("${suffix}"#,##0.00)`,
|
||||
color: 'red',
|
||||
},
|
||||
{
|
||||
label: (suffix: string) => `-${suffix}1,235`,
|
||||
suffix: (suffix: string) => `"${suffix}"#,##0.00_);-"${suffix}"#,##0.00`,
|
||||
},
|
||||
{
|
||||
label: (suffix: string) => `-${suffix}1,235`,
|
||||
suffix: (suffix: string) => `"${suffix}"#,##0.00_);[Red]-"${suffix}"#,##0.00`,
|
||||
color: 'red',
|
||||
},
|
||||
];
|
||||
17
packages/sheets-numfmt/src/base/const/plugin-name.ts
Normal file
17
packages/sheets-numfmt/src/base/const/plugin-name.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* 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 SHEET_NUMFMT_PLUGIN = 'SHEET_NUMFMT_PLUGIN';
|
||||
@@ -0,0 +1,370 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* 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 { Dependency, IWorkbookData, Injector as RediInjector, Workbook } from '@univerjs/core';
|
||||
import {
|
||||
CellValueType,
|
||||
DEFAULT_TEXT_FORMAT_EXCEL,
|
||||
ICommandService,
|
||||
ILogService,
|
||||
Inject,
|
||||
Injector,
|
||||
IUniverInstanceService,
|
||||
LocaleService,
|
||||
LocaleType,
|
||||
LogLevel,
|
||||
Plugin,
|
||||
RANGE_TYPE,
|
||||
RedoCommand,
|
||||
RegionService,
|
||||
UndoCommand,
|
||||
Univer,
|
||||
UniverInstanceType,
|
||||
} from '@univerjs/core';
|
||||
import {
|
||||
INumfmtService,
|
||||
NumfmtService,
|
||||
RemoveNumfmtMutation,
|
||||
SetNumfmtMutation,
|
||||
SetRangeValuesMutation,
|
||||
SheetsSelectionsService,
|
||||
} from '@univerjs/sheets';
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
import { getCurrencyFormat } from '../../../base/const/currency-symbols';
|
||||
import { AddDecimalCommand } from '../add-decimal.command';
|
||||
import { SetCurrencyCommand } from '../set-currency.command';
|
||||
import { SetNumfmtCommand } from '../set-numfmt.command';
|
||||
import { SetPercentCommand } from '../set-percent.command';
|
||||
import { SubtractDecimalCommand } from '../subtract-decimal.command';
|
||||
|
||||
function createWorkbookData(): IWorkbookData {
|
||||
return {
|
||||
id: 'test',
|
||||
appVersion: '3.0.0-alpha',
|
||||
locale: LocaleType.ZH_CN,
|
||||
name: '',
|
||||
sheetOrder: ['sheet1'],
|
||||
styles: {
|
||||
textStyle: {
|
||||
n: {
|
||||
pattern: DEFAULT_TEXT_FORMAT_EXCEL,
|
||||
},
|
||||
},
|
||||
twoDecimals: {
|
||||
n: {
|
||||
pattern: '0.00',
|
||||
},
|
||||
},
|
||||
},
|
||||
sheets: {
|
||||
sheet1: {
|
||||
id: 'sheet1',
|
||||
name: 'Sheet1',
|
||||
rowCount: 20,
|
||||
columnCount: 20,
|
||||
cellData: {
|
||||
0: {
|
||||
0: { v: 1.2, t: CellValueType.NUMBER },
|
||||
1: { v: 123, t: CellValueType.STRING, s: 'textStyle' },
|
||||
2: { v: 12, t: CellValueType.NUMBER },
|
||||
3: { v: 0.375, t: CellValueType.NUMBER },
|
||||
4: { v: 8, t: CellValueType.NUMBER },
|
||||
5: { v: 1e21, t: CellValueType.NUMBER },
|
||||
6: { v: 1.2345e31, t: CellValueType.NUMBER },
|
||||
},
|
||||
1: {
|
||||
0: { v: 1.23, t: CellValueType.NUMBER, s: 'twoDecimals' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createCommandTestBed(workbookData?: IWorkbookData) {
|
||||
const univer = new Univer();
|
||||
const injector = univer.__getInjector();
|
||||
|
||||
class TestPlugin extends Plugin {
|
||||
static override pluginName = 'test-plugin';
|
||||
static override type = UniverInstanceType.UNIVER_SHEET;
|
||||
|
||||
constructor(
|
||||
_config: undefined,
|
||||
@Inject(Injector) override readonly _injector: RediInjector
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
override onStarting(): void {
|
||||
const dependencies: Dependency[] = [
|
||||
[SheetsSelectionsService],
|
||||
[INumfmtService, { useClass: NumfmtService }],
|
||||
];
|
||||
|
||||
dependencies.forEach((dependency) => this._injector.add(dependency));
|
||||
}
|
||||
}
|
||||
|
||||
univer.registerPlugin(TestPlugin);
|
||||
const sheet = univer.createUnit<IWorkbookData, Workbook>(UniverInstanceType.UNIVER_SHEET, workbookData ?? createWorkbookData());
|
||||
|
||||
const get = injector.get.bind(injector);
|
||||
get(IUniverInstanceService).focusUnit('test');
|
||||
get(LocaleService).setLocale(LocaleType.ZH_CN);
|
||||
get(ILogService).setLogLevel(LogLevel.SILENT);
|
||||
|
||||
return {
|
||||
univer,
|
||||
get,
|
||||
sheet,
|
||||
};
|
||||
}
|
||||
|
||||
describe('Sheets numfmt commands', () => {
|
||||
let univer: Univer;
|
||||
let get: RediInjector['get'];
|
||||
let commandService: ICommandService;
|
||||
let selectionService: SheetsSelectionsService;
|
||||
let numfmtService: INumfmtService;
|
||||
let workbook: Workbook;
|
||||
|
||||
beforeEach(() => {
|
||||
const testBed = createCommandTestBed();
|
||||
univer = testBed.univer;
|
||||
get = testBed.get;
|
||||
workbook = testBed.sheet;
|
||||
|
||||
commandService = get(ICommandService);
|
||||
selectionService = get(SheetsSelectionsService);
|
||||
numfmtService = get(INumfmtService);
|
||||
|
||||
[
|
||||
SetRangeValuesMutation,
|
||||
SetNumfmtMutation,
|
||||
RemoveNumfmtMutation,
|
||||
SetNumfmtCommand,
|
||||
AddDecimalCommand,
|
||||
SubtractDecimalCommand,
|
||||
SetPercentCommand,
|
||||
SetCurrencyCommand,
|
||||
].forEach((command) => {
|
||||
commandService.registerCommand(command);
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
univer.dispose();
|
||||
});
|
||||
|
||||
it('sets and removes number formats through SetNumfmtCommand and supports undo/redo', async () => {
|
||||
const sheet = workbook.getActiveSheet();
|
||||
|
||||
await expect(commandService.executeCommand(SetNumfmtCommand.id, {
|
||||
values: [
|
||||
{ row: 0, col: 0, pattern: DEFAULT_TEXT_FORMAT_EXCEL },
|
||||
{ row: 0, col: 1 },
|
||||
],
|
||||
})).resolves.toBe(true);
|
||||
|
||||
expect(numfmtService.getValue('test', 'sheet1', 0, 0)).toEqual({ pattern: DEFAULT_TEXT_FORMAT_EXCEL });
|
||||
expect(numfmtService.getValue('test', 'sheet1', 0, 1)).toBeNull();
|
||||
expect(sheet.getCellRaw(0, 0)?.t).toBe(CellValueType.NUMBER);
|
||||
expect(sheet.getCellRaw(0, 1)).toMatchObject({ v: 123, t: CellValueType.NUMBER });
|
||||
|
||||
await expect(commandService.executeCommand(UndoCommand.id)).resolves.toBe(true);
|
||||
|
||||
expect(numfmtService.getValue('test', 'sheet1', 0, 0)).toBeNull();
|
||||
expect(numfmtService.getValue('test', 'sheet1', 0, 1)).toEqual({ pattern: DEFAULT_TEXT_FORMAT_EXCEL });
|
||||
expect(sheet.getCellRaw(0, 1)).toMatchObject({ v: '123', t: CellValueType.STRING });
|
||||
|
||||
await expect(commandService.executeCommand(RedoCommand.id)).resolves.toBe(true);
|
||||
|
||||
expect(numfmtService.getValue('test', 'sheet1', 0, 0)).toEqual({ pattern: DEFAULT_TEXT_FORMAT_EXCEL });
|
||||
expect(numfmtService.getValue('test', 'sheet1', 0, 1)).toBeNull();
|
||||
});
|
||||
|
||||
it('only allows general, text, and scientific formats for scientific notation numeric values', async () => {
|
||||
await expect(commandService.executeCommand(SetNumfmtCommand.id, {
|
||||
values: [{ row: 0, col: 5, pattern: '0.00' }],
|
||||
})).resolves.toBe(false);
|
||||
expect(numfmtService.getValue('test', 'sheet1', 0, 5)).toBeNull();
|
||||
|
||||
await expect(commandService.executeCommand(SetNumfmtCommand.id, {
|
||||
values: [{ row: 0, col: 5, pattern: '0.00E+00' }],
|
||||
})).resolves.toBe(true);
|
||||
expect(numfmtService.getValue('test', 'sheet1', 0, 5)).toEqual({ pattern: '0.00E+00' });
|
||||
|
||||
await expect(commandService.executeCommand(SetNumfmtCommand.id, {
|
||||
values: [{ row: 0, col: 5, pattern: DEFAULT_TEXT_FORMAT_EXCEL }],
|
||||
})).resolves.toBe(true);
|
||||
expect(numfmtService.getValue('test', 'sheet1', 0, 5)).toEqual({ pattern: DEFAULT_TEXT_FORMAT_EXCEL });
|
||||
|
||||
await expect(commandService.executeCommand(SetNumfmtCommand.id, {
|
||||
values: [{ row: 0, col: 5 }],
|
||||
})).resolves.toBe(true);
|
||||
expect(numfmtService.getValue('test', 'sheet1', 0, 5)).toBeNull();
|
||||
});
|
||||
|
||||
it('adds decimals to general scientific notation numeric values with a scientific format', async () => {
|
||||
selectionService.addSelections([
|
||||
{
|
||||
range: { startRow: 0, startColumn: 5, endRow: 0, endColumn: 5, rangeType: RANGE_TYPE.NORMAL },
|
||||
primary: null,
|
||||
style: null,
|
||||
},
|
||||
]);
|
||||
|
||||
await expect(commandService.executeCommand(AddDecimalCommand.id)).resolves.toBe(true);
|
||||
|
||||
expect(numfmtService.getValue('test', 'sheet1', 0, 5)).toEqual({ pattern: '0.000E+00' });
|
||||
});
|
||||
|
||||
it('adds decimals based on the current mantissa decimals for general scientific notation numeric values', async () => {
|
||||
selectionService.addSelections([
|
||||
{
|
||||
range: { startRow: 0, startColumn: 6, endRow: 0, endColumn: 6, rangeType: RANGE_TYPE.NORMAL },
|
||||
primary: null,
|
||||
style: null,
|
||||
},
|
||||
]);
|
||||
|
||||
await expect(commandService.executeCommand(AddDecimalCommand.id)).resolves.toBe(true);
|
||||
|
||||
expect(numfmtService.getValue('test', 'sheet1', 0, 6)).toEqual({ pattern: '0.00000E+00' });
|
||||
});
|
||||
|
||||
it('adds decimals to mixed general numbers without forcing regular numbers into scientific format', async () => {
|
||||
selectionService.addSelections([
|
||||
{
|
||||
range: { startRow: 0, startColumn: 0, endRow: 0, endColumn: 0, rangeType: RANGE_TYPE.NORMAL },
|
||||
primary: null,
|
||||
style: null,
|
||||
},
|
||||
{
|
||||
range: { startRow: 0, startColumn: 6, endRow: 0, endColumn: 6, rangeType: RANGE_TYPE.NORMAL },
|
||||
primary: null,
|
||||
style: null,
|
||||
},
|
||||
]);
|
||||
|
||||
await expect(commandService.executeCommand(AddDecimalCommand.id)).resolves.toBe(true);
|
||||
|
||||
expect(numfmtService.getValue('test', 'sheet1', 0, 0)).toEqual({ pattern: '0.00000' });
|
||||
expect(numfmtService.getValue('test', 'sheet1', 0, 6)).toEqual({ pattern: '0.00000E+00' });
|
||||
});
|
||||
|
||||
it('subtracts decimals from general scientific notation numeric values with a scientific format', async () => {
|
||||
selectionService.addSelections([
|
||||
{
|
||||
range: { startRow: 0, startColumn: 5, endRow: 0, endColumn: 5, rangeType: RANGE_TYPE.NORMAL },
|
||||
primary: null,
|
||||
style: null,
|
||||
},
|
||||
]);
|
||||
|
||||
await expect(commandService.executeCommand(SubtractDecimalCommand.id)).resolves.toBe(true);
|
||||
|
||||
expect(numfmtService.getValue('test', 'sheet1', 0, 5)).toEqual({ pattern: '0.0E+00' });
|
||||
});
|
||||
|
||||
it('subtracts decimals based on the current mantissa decimals for general scientific notation numeric values', async () => {
|
||||
selectionService.addSelections([
|
||||
{
|
||||
range: { startRow: 0, startColumn: 6, endRow: 0, endColumn: 6, rangeType: RANGE_TYPE.NORMAL },
|
||||
primary: null,
|
||||
style: null,
|
||||
},
|
||||
]);
|
||||
|
||||
await expect(commandService.executeCommand(SubtractDecimalCommand.id)).resolves.toBe(true);
|
||||
|
||||
expect(numfmtService.getValue('test', 'sheet1', 0, 6)).toEqual({ pattern: '0.000E+00' });
|
||||
});
|
||||
|
||||
it('adds and subtracts decimals based on selections and existing formats', async () => {
|
||||
selectionService.addSelections([
|
||||
{
|
||||
range: { startRow: 0, startColumn: 0, endRow: 0, endColumn: 0, rangeType: RANGE_TYPE.NORMAL },
|
||||
primary: null,
|
||||
style: null,
|
||||
},
|
||||
{
|
||||
range: { startRow: 1, startColumn: 0, endRow: 1, endColumn: 0, rangeType: RANGE_TYPE.NORMAL },
|
||||
primary: null,
|
||||
style: null,
|
||||
},
|
||||
]);
|
||||
|
||||
await expect(commandService.executeCommand(AddDecimalCommand.id)).resolves.toBe(true);
|
||||
|
||||
expect(numfmtService.getValue('test', 'sheet1', 0, 0)).toEqual({ pattern: '0.000' });
|
||||
expect(numfmtService.getValue('test', 'sheet1', 1, 0)).toEqual({ pattern: '0.000' });
|
||||
|
||||
await expect(commandService.executeCommand(SubtractDecimalCommand.id)).resolves.toBe(true);
|
||||
|
||||
expect(numfmtService.getValue('test', 'sheet1', 0, 0)).toEqual({ pattern: '0.00' });
|
||||
expect(numfmtService.getValue('test', 'sheet1', 1, 0)).toEqual({ pattern: '0.00' });
|
||||
});
|
||||
|
||||
it('subtracts decimals from raw numeric cells when no number format exists yet', async () => {
|
||||
selectionService.addSelections([
|
||||
{
|
||||
range: { startRow: 0, startColumn: 0, endRow: 0, endColumn: 0, rangeType: RANGE_TYPE.NORMAL },
|
||||
primary: null,
|
||||
style: null,
|
||||
},
|
||||
]);
|
||||
|
||||
await expect(commandService.executeCommand(SubtractDecimalCommand.id)).resolves.toBe(true);
|
||||
|
||||
expect(numfmtService.getValue('test', 'sheet1', 0, 0)).toEqual({ pattern: '0' });
|
||||
});
|
||||
|
||||
it('applies percent and currency formats to current selections', async () => {
|
||||
selectionService.addSelections([
|
||||
{
|
||||
range: { startRow: 0, startColumn: 3, endRow: 0, endColumn: 3, rangeType: RANGE_TYPE.NORMAL },
|
||||
primary: null,
|
||||
style: null,
|
||||
},
|
||||
]);
|
||||
|
||||
await expect(commandService.executeCommand(SetPercentCommand.id)).resolves.toBe(true);
|
||||
expect(numfmtService.getValue('test', 'sheet1', 0, 3)).toEqual({ pattern: '0%' });
|
||||
|
||||
selectionService.addSelections([
|
||||
{
|
||||
range: { startRow: 0, startColumn: 4, endRow: 0, endColumn: 4, rangeType: RANGE_TYPE.NORMAL },
|
||||
primary: null,
|
||||
style: null,
|
||||
},
|
||||
]);
|
||||
|
||||
get(RegionService).setRegion(LocaleType.FR_FR);
|
||||
await expect(commandService.executeCommand(SetCurrencyCommand.id)).resolves.toBe(true);
|
||||
expect(numfmtService.getValue('test', 'sheet1', 0, 4)).toEqual({
|
||||
pattern: getCurrencyFormat(LocaleType.FR_FR),
|
||||
});
|
||||
});
|
||||
|
||||
it('returns false for quick commands when there is no current selection', async () => {
|
||||
await expect(commandService.executeCommand(AddDecimalCommand.id)).resolves.toBe(false);
|
||||
await expect(commandService.executeCommand(SubtractDecimalCommand.id)).resolves.toBe(false);
|
||||
await expect(commandService.executeCommand(SetPercentCommand.id)).resolves.toBe(false);
|
||||
await expect(commandService.executeCommand(SetCurrencyCommand.id)).resolves.toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* 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 { IAccessor, ICommand } from '@univerjs/core';
|
||||
import type { INumfmtItemWithCache } from '@univerjs/sheets';
|
||||
import type { ISetNumfmtCommandParams } from './set-numfmt.command';
|
||||
import { CellValueType, CommandType, ICommandService, isDefaultFormat, IUniverInstanceService, Range } from '@univerjs/core';
|
||||
import { getSheetCommandTarget, INumfmtService, SheetsSelectionsService } from '@univerjs/sheets';
|
||||
import { getDecimalFromPattern, setPatternDecimal } from '../../utils/decimal';
|
||||
import { getScientificNotationFormatFromCell, isScientificNotationNumericCell } from '../../utils/scientific-notation';
|
||||
import { SetNumfmtCommand } from './set-numfmt.command';
|
||||
|
||||
export const AddDecimalCommand: ICommand = {
|
||||
id: 'sheet.command.numfmt.add.decimal.command',
|
||||
type: CommandType.COMMAND,
|
||||
handler: async (accessor: IAccessor) => {
|
||||
const commandService = accessor.get(ICommandService);
|
||||
const selectionManagerService = accessor.get(SheetsSelectionsService);
|
||||
const numfmtService = accessor.get(INumfmtService);
|
||||
const univerInstanceService = accessor.get(IUniverInstanceService);
|
||||
const selections = selectionManagerService.getCurrentSelections();
|
||||
if (!selections || !selections.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const target = getSheetCommandTarget(univerInstanceService);
|
||||
if (!target) return false;
|
||||
|
||||
const { unitId, subUnitId } = target;
|
||||
|
||||
let maxDecimals = 0;
|
||||
selections.forEach((selection) => {
|
||||
Range.foreach(selection.range, (row, col) => {
|
||||
const numfmtValue = numfmtService.getValue(unitId, subUnitId, row, col);
|
||||
if (!numfmtValue) {
|
||||
const cell = target.worksheet.getCellRaw(row, col);
|
||||
if (isScientificNotationNumericCell(cell)) {
|
||||
maxDecimals = Math.max(maxDecimals, getDecimalFromPattern(getScientificNotationFormatFromCell(cell)));
|
||||
return;
|
||||
}
|
||||
if (!maxDecimals && cell && cell.t === CellValueType.NUMBER && cell.v) {
|
||||
const regResult = /\.(\d*)$/.exec(String(cell.v));
|
||||
if (regResult) {
|
||||
const length = regResult[1].length;
|
||||
if (!length) {
|
||||
return;
|
||||
}
|
||||
maxDecimals = Math.max(maxDecimals, length);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
const decimals = getDecimalFromPattern(numfmtValue.pattern);
|
||||
maxDecimals = decimals > maxDecimals ? decimals : maxDecimals;
|
||||
});
|
||||
});
|
||||
const decimals = maxDecimals + 1;
|
||||
const defaultPattern = setPatternDecimal(`0${decimals > 0 ? '.0' : ''}`, decimals);
|
||||
const values: ISetNumfmtCommandParams['values'] = [];
|
||||
|
||||
selections.forEach((selection) => {
|
||||
Range.foreach(selection.range, (row, col) => {
|
||||
const numfmtValue = numfmtService.getValue(unitId, subUnitId, row, col);
|
||||
if (isDefaultFormat(numfmtValue?.pattern)) {
|
||||
const cell = target.worksheet.getCellRaw(row, col);
|
||||
// General scientific notation numbers switch to scientific format before changing decimals.
|
||||
values.push({
|
||||
row,
|
||||
col,
|
||||
pattern: isScientificNotationNumericCell(cell) ? setPatternDecimal(getScientificNotationFormatFromCell(cell), decimals) : defaultPattern,
|
||||
});
|
||||
} else {
|
||||
const decimals = getDecimalFromPattern((numfmtValue as INumfmtItemWithCache).pattern);
|
||||
const pattern = setPatternDecimal((numfmtValue as INumfmtItemWithCache).pattern, decimals + 1);
|
||||
pattern !== (numfmtValue as INumfmtItemWithCache).pattern &&
|
||||
values.push({
|
||||
row,
|
||||
col,
|
||||
pattern,
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
if (values.length) {
|
||||
const result = await commandService.executeCommand(SetNumfmtCommand.id, { values });
|
||||
return result;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* 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 { IAccessor, ICommand } from '@univerjs/core';
|
||||
import type { ISetNumfmtCommandParams } from './set-numfmt.command';
|
||||
import { CommandType, ICommandService, Range, RegionService } from '@univerjs/core';
|
||||
import { SheetsSelectionsService } from '@univerjs/sheets';
|
||||
import { getCurrencyFormat, getCurrencySymbolIconByLocale } from '../../base/const/currency-symbols';
|
||||
import { SetNumfmtCommand } from './set-numfmt.command';
|
||||
|
||||
/**
|
||||
* This command is triggered by clicking the currency symbol icon in the menu.
|
||||
* So the currency format is determined by the currency symbol icon.
|
||||
*/
|
||||
export const SetCurrencyCommand: ICommand = {
|
||||
id: 'sheet.command.numfmt.set.currency',
|
||||
type: CommandType.COMMAND,
|
||||
handler: async (accessor: IAccessor) => {
|
||||
const commandService = accessor.get(ICommandService);
|
||||
const selectionManagerService = accessor.get(SheetsSelectionsService);
|
||||
const regionService = accessor.get(RegionService);
|
||||
|
||||
const selections = selectionManagerService.getCurrentSelections();
|
||||
if (!selections || !selections.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const values: ISetNumfmtCommandParams['values'] = [];
|
||||
const currencySymbolIcon = getCurrencySymbolIconByLocale(regionService.getCurrentRegion());
|
||||
const currencyFormat = getCurrencyFormat(currencySymbolIcon.locale);
|
||||
|
||||
selections.forEach((selection) => {
|
||||
Range.foreach(selection.range, (row, col) => {
|
||||
values.push({ row, col, pattern: currencyFormat, type: 'currency' });
|
||||
});
|
||||
});
|
||||
|
||||
const result = await commandService.executeCommand(SetNumfmtCommand.id, { values });
|
||||
return result;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,188 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* 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 { IAccessor, ICommand, IMutationInfo, Nullable } from '@univerjs/core';
|
||||
import type {
|
||||
FormatType,
|
||||
IRemoveNumfmtMutationParams,
|
||||
ISetCellsNumfmt,
|
||||
ISetNumfmtMutationParams,
|
||||
ISetRangeValuesMutationParams,
|
||||
} from '@univerjs/sheets';
|
||||
import {
|
||||
CellValueType,
|
||||
CommandType,
|
||||
ICommandService,
|
||||
isTextFormat,
|
||||
IUndoRedoService,
|
||||
IUniverInstanceService,
|
||||
ObjectMatrix,
|
||||
sequenceExecute,
|
||||
} from '@univerjs/core';
|
||||
import {
|
||||
checkCellValueType,
|
||||
factoryRemoveNumfmtUndoMutation,
|
||||
factorySetNumfmtUndoMutation,
|
||||
getSheetCommandTarget,
|
||||
rangeMerge,
|
||||
RemoveNumfmtMutation,
|
||||
SetNumfmtMutation,
|
||||
SetRangeValuesMutation,
|
||||
transformCellsToRange,
|
||||
} from '@univerjs/sheets';
|
||||
import { isAllowedPatternForScientificNotationNumber, isScientificNotationNumericCell } from '../../utils/scientific-notation';
|
||||
|
||||
export interface ISetNumfmtCommandParams {
|
||||
unitId?: string;
|
||||
subUnitId?: string;
|
||||
values: Array<{ pattern?: string; row: number; col: number; type?: FormatType }>;
|
||||
}
|
||||
|
||||
export const SetNumfmtCommand: ICommand<ISetNumfmtCommandParams> = {
|
||||
id: 'sheet.command.numfmt.set.numfmt',
|
||||
type: CommandType.COMMAND,
|
||||
// eslint-disable-next-line max-lines-per-function
|
||||
handler: (accessor: IAccessor, params) => {
|
||||
if (!params) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const commandService = accessor.get(ICommandService);
|
||||
const univerInstanceService = accessor.get(IUniverInstanceService);
|
||||
const undoRedoService = accessor.get(IUndoRedoService);
|
||||
|
||||
const target = getSheetCommandTarget(univerInstanceService, params);
|
||||
if (!target) return false;
|
||||
|
||||
const { unitId, subUnitId, worksheet } = target;
|
||||
// Because of precision issues with scientific notation values, filter out some format settings for them here.
|
||||
const values = params.values.filter((value) => {
|
||||
const cell = worksheet.getCellRaw(value.row, value.col);
|
||||
return !isScientificNotationNumericCell(cell) || isAllowedPatternForScientificNotationNumber(value.pattern);
|
||||
});
|
||||
const setCells = values.filter((value) => !!value.pattern) as ISetCellsNumfmt;
|
||||
const removeCells = values.filter((value) => !value.pattern);
|
||||
if (!setCells.length && !removeCells.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const setRedos = transformCellsToRange(unitId, subUnitId, setCells);
|
||||
const removeRedos: IRemoveNumfmtMutationParams = {
|
||||
unitId,
|
||||
subUnitId,
|
||||
ranges: removeCells.map((cell) => ({
|
||||
startColumn: cell.col,
|
||||
startRow: cell.row,
|
||||
endColumn: cell.col,
|
||||
endRow: cell.row,
|
||||
})),
|
||||
};
|
||||
const redos: Array<IMutationInfo<IRemoveNumfmtMutationParams | ISetNumfmtMutationParams | ISetRangeValuesMutationParams>> = [];
|
||||
const undos: Array<IMutationInfo<IRemoveNumfmtMutationParams | ISetNumfmtMutationParams | ISetRangeValuesMutationParams>> = [];
|
||||
if (setCells.length) {
|
||||
const setCellTypeObj = setCells.reduce((pre, cur) => {
|
||||
if (isTextFormat(cur.pattern)) {
|
||||
pre.setValue(cur.row, cur.col, { t: CellValueType.STRING });
|
||||
}
|
||||
const cell = worksheet.getCellRaw(cur.row, cur.col);
|
||||
if (cell) {
|
||||
const type = checkCellValueType(cell.v);
|
||||
if (type !== cell.t) {
|
||||
pre.setValue(cur.row, cur.col, { t: type });
|
||||
}
|
||||
}
|
||||
return pre;
|
||||
}, new ObjectMatrix<{ t: Nullable<CellValueType> }>()).getMatrix();
|
||||
|
||||
const undoSetCellTypeObj = new ObjectMatrix<{ t: Nullable<CellValueType> }>();
|
||||
new ObjectMatrix<{ t: Nullable<CellValueType> }>(setCellTypeObj).forValue((row, col) => {
|
||||
const cell = worksheet.getCellRaw(row, col);
|
||||
if (cell) {
|
||||
undoSetCellTypeObj.setValue(row, col, { t: cell.t });
|
||||
} else {
|
||||
undoSetCellTypeObj.setValue(row, col, { t: undefined });
|
||||
}
|
||||
});
|
||||
Object.keys(setRedos.values).forEach((key) => {
|
||||
const v = setRedos.values[key];
|
||||
v.ranges = rangeMerge(v.ranges);
|
||||
});
|
||||
redos.push({
|
||||
id: SetNumfmtMutation.id,
|
||||
params: setRedos,
|
||||
});
|
||||
const undo = factorySetNumfmtUndoMutation(accessor, setRedos);
|
||||
undos.push(...undo);
|
||||
}
|
||||
|
||||
if (removeCells.length) {
|
||||
removeRedos.ranges = rangeMerge(removeRedos.ranges);
|
||||
|
||||
const setCellTypeObj = removeCells.reduce((pre, cur) => {
|
||||
const cell = worksheet.getCellRaw(cur.row, cur.col);
|
||||
if (cell) {
|
||||
const type = checkCellValueType(cell.v);
|
||||
if (type !== cell.t) {
|
||||
pre.setValue(cur.row, cur.col, { t: type });
|
||||
}
|
||||
}
|
||||
return pre;
|
||||
}, new ObjectMatrix<{ t: Nullable<CellValueType> }>()).getMatrix();
|
||||
|
||||
const undoSetCellTypeObj = new ObjectMatrix<{ t: Nullable<CellValueType> }>();
|
||||
new ObjectMatrix<{ t: Nullable<CellValueType> }>(setCellTypeObj).forValue((row, col) => {
|
||||
const cell = worksheet.getCellRaw(row, col);
|
||||
if (cell) {
|
||||
undoSetCellTypeObj.setValue(row, col, { t: cell.t });
|
||||
} else {
|
||||
undoSetCellTypeObj.setValue(row, col, { t: undefined });
|
||||
}
|
||||
});
|
||||
|
||||
redos.push({
|
||||
id: RemoveNumfmtMutation.id,
|
||||
params: removeRedos,
|
||||
}, {
|
||||
id: SetRangeValuesMutation.id,
|
||||
params: {
|
||||
unitId,
|
||||
subUnitId,
|
||||
cellValue: setCellTypeObj,
|
||||
},
|
||||
});
|
||||
const undo = factoryRemoveNumfmtUndoMutation(accessor, removeRedos);
|
||||
|
||||
undos.push({
|
||||
id: SetRangeValuesMutation.id,
|
||||
params: {
|
||||
unitId,
|
||||
subUnitId,
|
||||
cellValue: undoSetCellTypeObj.getMatrix(),
|
||||
},
|
||||
}, ...undo);
|
||||
}
|
||||
|
||||
const result = sequenceExecute(redos, commandService).result;
|
||||
if (result) {
|
||||
undoRedoService.pushUndoRedo({
|
||||
unitID: unitId,
|
||||
undoMutations: undos,
|
||||
redoMutations: redos,
|
||||
});
|
||||
}
|
||||
return result;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* 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 { IAccessor, ICommand } from '@univerjs/core';
|
||||
import type { ISetNumfmtCommandParams } from './set-numfmt.command';
|
||||
import { CommandType, ICommandService, Range } from '@univerjs/core';
|
||||
import { SheetsSelectionsService } from '@univerjs/sheets';
|
||||
import { SetNumfmtCommand } from './set-numfmt.command';
|
||||
|
||||
export const SetPercentCommand: ICommand = {
|
||||
id: 'sheet.command.numfmt.set.percent',
|
||||
type: CommandType.COMMAND,
|
||||
handler: async (accessor: IAccessor) => {
|
||||
const commandService = accessor.get(ICommandService);
|
||||
const selectionManagerService = accessor.get(SheetsSelectionsService);
|
||||
|
||||
const selections = selectionManagerService.getCurrentSelections();
|
||||
if (!selections || !selections.length) {
|
||||
return false;
|
||||
}
|
||||
const values: ISetNumfmtCommandParams['values'] = [];
|
||||
|
||||
const suffix = '0%';
|
||||
|
||||
selections.forEach((selection) => {
|
||||
Range.foreach(selection.range, (row, col) => {
|
||||
values.push({ row, col, pattern: suffix, type: 'percent' });
|
||||
});
|
||||
});
|
||||
const result = await commandService.executeCommand(SetNumfmtCommand.id, { values });
|
||||
return result;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,101 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* 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 { IAccessor, ICommand } from '@univerjs/core';
|
||||
import type { INumfmtItemWithCache } from '@univerjs/sheets';
|
||||
import type { ISetNumfmtCommandParams } from './set-numfmt.command';
|
||||
import { CellValueType, CommandType, ICommandService, isDefaultFormat, IUniverInstanceService, Range } from '@univerjs/core';
|
||||
import { getSheetCommandTarget, INumfmtService, SheetsSelectionsService } from '@univerjs/sheets';
|
||||
import { getDecimalFromPattern, setPatternDecimal } from '../../utils/decimal';
|
||||
import { getScientificNotationFormatFromCell, isScientificNotationNumericCell } from '../../utils/scientific-notation';
|
||||
import { SetNumfmtCommand } from './set-numfmt.command';
|
||||
|
||||
export const SubtractDecimalCommand: ICommand = {
|
||||
id: 'sheet.command.numfmt.subtract.decimal.command',
|
||||
type: CommandType.COMMAND,
|
||||
handler: async (accessor: IAccessor) => {
|
||||
const commandService = accessor.get(ICommandService);
|
||||
const selectionManagerService = accessor.get(SheetsSelectionsService);
|
||||
const numfmtService = accessor.get(INumfmtService);
|
||||
const univerInstanceService = accessor.get(IUniverInstanceService);
|
||||
|
||||
const selections = selectionManagerService.getCurrentSelections();
|
||||
if (!selections || !selections.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const target = getSheetCommandTarget(univerInstanceService);
|
||||
if (!target) return false;
|
||||
|
||||
const { unitId, subUnitId } = target;
|
||||
|
||||
let maxDecimals = 0;
|
||||
selections.forEach((selection) => {
|
||||
Range.foreach(selection.range, (row, col) => {
|
||||
const numfmtValue = numfmtService.getValue(unitId, subUnitId, row, col);
|
||||
if (!numfmtValue) {
|
||||
const cell = target.worksheet.getCellRaw(row, col);
|
||||
if (isScientificNotationNumericCell(cell)) {
|
||||
maxDecimals = Math.max(maxDecimals, getDecimalFromPattern(getScientificNotationFormatFromCell(cell)));
|
||||
return;
|
||||
}
|
||||
if (!maxDecimals && cell && cell.t === CellValueType.NUMBER && cell.v) {
|
||||
const regResult = /\.(\d*)$/.exec(String(cell.v));
|
||||
if (regResult) {
|
||||
const length = regResult[1].length;
|
||||
if (!length) {
|
||||
return;
|
||||
}
|
||||
maxDecimals = Math.max(maxDecimals, length);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
const decimals = getDecimalFromPattern(numfmtValue.pattern);
|
||||
maxDecimals = decimals > maxDecimals ? decimals : maxDecimals;
|
||||
});
|
||||
});
|
||||
const decimals = maxDecimals - 1;
|
||||
|
||||
const defaultPattern = setPatternDecimal(`0${decimals > 0 ? '.0' : '.'}`, decimals);
|
||||
const values: ISetNumfmtCommandParams['values'] = [];
|
||||
|
||||
selections.forEach((selection) => {
|
||||
Range.foreach(selection.range, (row, col) => {
|
||||
const numfmtValue = numfmtService.getValue(unitId, subUnitId, row, col);
|
||||
if (isDefaultFormat(numfmtValue?.pattern)) {
|
||||
const cell = target.worksheet.getCellRaw(row, col);
|
||||
// General scientific notation numbers switch to scientific format before changing decimals.
|
||||
values.push({
|
||||
row,
|
||||
col,
|
||||
pattern: isScientificNotationNumericCell(cell) ? setPatternDecimal(getScientificNotationFormatFromCell(cell), decimals) : defaultPattern,
|
||||
});
|
||||
} else {
|
||||
const decimals = getDecimalFromPattern((numfmtValue as INumfmtItemWithCache).pattern);
|
||||
values.push({
|
||||
row,
|
||||
col,
|
||||
pattern: setPatternDecimal((numfmtValue as INumfmtItemWithCache).pattern, decimals - 1),
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const result = await commandService.executeCommand(SetNumfmtCommand.id, { values });
|
||||
return result;
|
||||
},
|
||||
};
|
||||
32
packages/sheets-numfmt/src/config/config.ts
Normal file
32
packages/sheets-numfmt/src/config/config.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* 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 SHEETS_NUMFMT_PLUGIN_CONFIG_KEY = 'sheets-numfmt.config';
|
||||
|
||||
export const configSymbol = Symbol(SHEETS_NUMFMT_PLUGIN_CONFIG_KEY);
|
||||
|
||||
export interface IUniverSheetsNumfmtConfig {
|
||||
/**
|
||||
* Whether to disable the text format alert.
|
||||
*/
|
||||
disableTextFormatAlert?: boolean;
|
||||
/**
|
||||
* Whether to disable the text format mark.
|
||||
*/
|
||||
disableTextFormatMark?: boolean;
|
||||
}
|
||||
|
||||
export const defaultPluginConfig: IUniverSheetsNumfmtConfig = {};
|
||||
@@ -0,0 +1,364 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* 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 { Dependency, IWorkbookData, Workbook, Worksheet } from '@univerjs/core';
|
||||
import {
|
||||
cellToRange,
|
||||
CellValueType,
|
||||
DateSystem,
|
||||
ICommandService,
|
||||
IConfigService,
|
||||
ILogService,
|
||||
Inject,
|
||||
Injector,
|
||||
InterceptorEffectEnum,
|
||||
IUniverInstanceService,
|
||||
LocaleType,
|
||||
LogLevel,
|
||||
Plugin,
|
||||
Univer,
|
||||
UniverInstanceType,
|
||||
} from '@univerjs/core';
|
||||
import {
|
||||
INTERCEPTOR_POINT,
|
||||
INumfmtService,
|
||||
NumfmtService,
|
||||
SetNumfmtMutation,
|
||||
SetRangeValuesMutation,
|
||||
SheetInterceptorService,
|
||||
} from '@univerjs/sheets';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { SHEETS_NUMFMT_PLUGIN_CONFIG_KEY } from '../../config/config';
|
||||
import * as patternUtils from '../../utils/pattern';
|
||||
import { SheetsNumfmtCellContentController } from '../numfmt-cell-content.controller';
|
||||
|
||||
function createWorkbookData(
|
||||
id = 'test',
|
||||
locale = LocaleType.ZH_CN,
|
||||
dateSystem = DateSystem.Date1900
|
||||
): IWorkbookData {
|
||||
return {
|
||||
id,
|
||||
appVersion: '3.0.0-alpha',
|
||||
locale,
|
||||
dateSystem,
|
||||
name: '',
|
||||
sheetOrder: ['sheet1', 'sheet2'],
|
||||
styles: {
|
||||
textStyle: {
|
||||
n: {
|
||||
pattern: '@',
|
||||
},
|
||||
},
|
||||
},
|
||||
sheets: {
|
||||
sheet1: {
|
||||
id: 'sheet1',
|
||||
name: 'Sheet1',
|
||||
rowCount: 20,
|
||||
columnCount: 20,
|
||||
cellData: {
|
||||
0: {
|
||||
0: { v: true, t: CellValueType.BOOLEAN },
|
||||
1: { v: '001', t: CellValueType.FORCE_STRING },
|
||||
2: { v: 'text', t: CellValueType.STRING },
|
||||
},
|
||||
1: {
|
||||
0: { v: '123', t: CellValueType.STRING, s: 'textStyle' },
|
||||
1: { v: '1234.5', t: CellValueType.STRING },
|
||||
2: { v: -12.3, t: CellValueType.NUMBER },
|
||||
3: { v: '20%', t: CellValueType.STRING, s: 'textStyle' },
|
||||
4: { v: 0, t: CellValueType.NUMBER },
|
||||
},
|
||||
},
|
||||
},
|
||||
sheet2: {
|
||||
id: 'sheet2',
|
||||
name: 'Sheet2',
|
||||
rowCount: 20,
|
||||
columnCount: 20,
|
||||
cellData: {
|
||||
1: {
|
||||
1: { v: '9.9', t: CellValueType.STRING },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createControllerTestBed(workbookData?: IWorkbookData) {
|
||||
const univer = new Univer();
|
||||
const injector = univer.__getInjector();
|
||||
|
||||
class TestPlugin extends Plugin {
|
||||
static override pluginName = 'test-plugin';
|
||||
static override type = UniverInstanceType.UNIVER_SHEET;
|
||||
|
||||
constructor(
|
||||
_config: undefined,
|
||||
@Inject(Injector) override readonly _injector: Injector
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
override onStarting(): void {
|
||||
const dependencies: Dependency[] = [
|
||||
[SheetInterceptorService],
|
||||
[INumfmtService, { useClass: NumfmtService }],
|
||||
[SheetsNumfmtCellContentController],
|
||||
];
|
||||
|
||||
dependencies.forEach((dependency) => this._injector.add(dependency));
|
||||
}
|
||||
}
|
||||
|
||||
univer.registerPlugin(TestPlugin);
|
||||
const sheet = univer.createUnit<IWorkbookData, Workbook>(UniverInstanceType.UNIVER_SHEET, workbookData ?? createWorkbookData());
|
||||
|
||||
const get = injector.get.bind(injector);
|
||||
get(IUniverInstanceService).focusUnit('test');
|
||||
get(ILogService).setLogLevel(LogLevel.SILENT);
|
||||
|
||||
return {
|
||||
univer,
|
||||
get,
|
||||
sheet,
|
||||
};
|
||||
}
|
||||
|
||||
function getInterceptedCell(worksheet: Worksheet, workbook: Workbook, row: number, col: number, get: Injector['get']) {
|
||||
const interceptorService = get(SheetInterceptorService);
|
||||
const rawData = worksheet.getCellRaw(row, col);
|
||||
|
||||
return interceptorService.fetchThroughInterceptors(INTERCEPTOR_POINT.CELL_CONTENT, InterceptorEffectEnum.Style | InterceptorEffectEnum.Value)(
|
||||
rawData,
|
||||
{
|
||||
unitId: workbook.getUnitId(),
|
||||
subUnitId: worksheet.getSheetId(),
|
||||
row,
|
||||
col,
|
||||
worksheet,
|
||||
workbook,
|
||||
rawData,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
describe('SheetsNumfmtCellContentController', () => {
|
||||
let univer: Univer;
|
||||
let get: Injector['get'];
|
||||
let workbook: Workbook;
|
||||
let commandService: ICommandService;
|
||||
let numfmtService: INumfmtService;
|
||||
let controller: SheetsNumfmtCellContentController;
|
||||
|
||||
beforeEach(() => {
|
||||
const testBed = createControllerTestBed();
|
||||
univer = testBed.univer;
|
||||
get = testBed.get;
|
||||
workbook = testBed.sheet;
|
||||
|
||||
commandService = get(ICommandService);
|
||||
numfmtService = get(INumfmtService);
|
||||
controller = get(SheetsNumfmtCellContentController);
|
||||
|
||||
commandService.registerCommand(SetNumfmtMutation);
|
||||
commandService.registerCommand(SetRangeValuesMutation);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
univer.dispose();
|
||||
});
|
||||
|
||||
it('skips unsupported cells and handles text format markers', () => {
|
||||
const worksheet = workbook.getSheetBySheetId('sheet1')!;
|
||||
|
||||
expect(getInterceptedCell(worksheet, workbook, 0, 0, get)).toEqual({ v: true, t: CellValueType.BOOLEAN });
|
||||
expect(getInterceptedCell(worksheet, workbook, 0, 1, get)).toEqual({ v: '001', t: CellValueType.FORCE_STRING });
|
||||
expect(getInterceptedCell(worksheet, workbook, 0, 2, get)).toEqual({ v: 'text', t: CellValueType.STRING });
|
||||
|
||||
expect(getInterceptedCell(worksheet, workbook, 1, 0, get)).toEqual({
|
||||
v: '123',
|
||||
t: CellValueType.STRING,
|
||||
s: 'textStyle',
|
||||
markers: {
|
||||
tl: {
|
||||
size: 6,
|
||||
color: '#409f11',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(getInterceptedCell(worksheet, workbook, 1, 3, get)).toEqual({
|
||||
v: '20%',
|
||||
t: CellValueType.STRING,
|
||||
s: 'textStyle',
|
||||
markers: {
|
||||
tl: {
|
||||
size: 6,
|
||||
color: '#409f11',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
get(IConfigService).setConfig(SHEETS_NUMFMT_PLUGIN_CONFIG_KEY, { disableTextFormatMark: true });
|
||||
|
||||
expect(getInterceptedCell(worksheet, workbook, 1, 0, get)).toEqual({
|
||||
v: '123',
|
||||
t: CellValueType.STRING,
|
||||
s: 'textStyle',
|
||||
});
|
||||
expect(getInterceptedCell(worksheet, workbook, 1, 3, get)).toEqual({
|
||||
v: '20%',
|
||||
t: CellValueType.STRING,
|
||||
s: 'textStyle',
|
||||
});
|
||||
});
|
||||
|
||||
it('formats numeric values and clears render cache on numfmt or value mutations', async () => {
|
||||
const worksheet = workbook.getSheetBySheetId('sheet1')!;
|
||||
const previewSpy = vi.spyOn(patternUtils, 'getPatternPreviewIgnoreGeneral');
|
||||
|
||||
numfmtService.setValues('test', 'sheet1', [
|
||||
{ pattern: '0.00', ranges: [cellToRange(1, 1)] },
|
||||
{ pattern: '[Red]0.00', ranges: [cellToRange(1, 2)] },
|
||||
]);
|
||||
|
||||
const firstFormatted = getInterceptedCell(worksheet, workbook, 1, 1, get);
|
||||
expect(firstFormatted).toMatchObject({ v: '1234.50', t: CellValueType.NUMBER });
|
||||
expect(worksheet.getCellRaw(1, 1)).toMatchObject({ v: '1234.5', t: CellValueType.STRING });
|
||||
|
||||
const negativeFormatted = getInterceptedCell(worksheet, workbook, 1, 2, get);
|
||||
expect(negativeFormatted).toMatchObject({
|
||||
v: '-12.30',
|
||||
t: CellValueType.NUMBER,
|
||||
interceptorStyle: {
|
||||
cl: {
|
||||
rgb: expect.any(String),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const previewCallsAfterFirstRender = previewSpy.mock.calls.length;
|
||||
|
||||
getInterceptedCell(worksheet, workbook, 1, 1, get);
|
||||
expect(previewSpy).toHaveBeenCalledTimes(previewCallsAfterFirstRender);
|
||||
|
||||
await commandService.executeCommand(SetNumfmtMutation.id, {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
refMap: {
|
||||
1: {
|
||||
pattern: '0.00',
|
||||
},
|
||||
},
|
||||
values: {
|
||||
1: {
|
||||
ranges: [cellToRange(1, 1)],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
getInterceptedCell(worksheet, workbook, 1, 1, get);
|
||||
expect(previewSpy).toHaveBeenCalledTimes(previewCallsAfterFirstRender + 1);
|
||||
|
||||
await commandService.executeCommand(SetRangeValuesMutation.id, {
|
||||
unitId: 'test',
|
||||
subUnitId: 'sheet1',
|
||||
cellValue: {
|
||||
1: {
|
||||
1: {
|
||||
v: '1234.5',
|
||||
t: CellValueType.STRING,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
getInterceptedCell(worksheet, workbook, 1, 1, get);
|
||||
expect(previewSpy).toHaveBeenCalledTimes(previewCallsAfterFirstRender + 2);
|
||||
});
|
||||
|
||||
it('formats each workbook with its own locale and date system', () => {
|
||||
const date1900Sheet = workbook.getSheetBySheetId('sheet1')!;
|
||||
const date1904Workbook = univer.createUnit<IWorkbookData, Workbook>(
|
||||
UniverInstanceType.UNIVER_SHEET,
|
||||
createWorkbookData('date1904', LocaleType.FR_FR, DateSystem.Date1904)
|
||||
);
|
||||
const date1904Sheet = date1904Workbook.getSheetBySheetId('sheet1')!;
|
||||
const previewSpy = vi.spyOn(patternUtils, 'getPatternPreviewIgnoreGeneral');
|
||||
|
||||
numfmtService.setValues('test', 'sheet1', [{ pattern: 'yyyy-mm-dd', ranges: [cellToRange(1, 4)] }]);
|
||||
numfmtService.setValues('date1904', 'sheet1', [{ pattern: 'yyyy-mm-dd', ranges: [cellToRange(1, 4)] }]);
|
||||
|
||||
expect(getInterceptedCell(date1900Sheet, workbook, 1, 4, get)).toMatchObject({ v: '1900-01-00' });
|
||||
expect(getInterceptedCell(date1904Sheet, date1904Workbook, 1, 4, get)).toMatchObject({ v: '1904-01-01' });
|
||||
expect(previewSpy).toHaveBeenLastCalledWith('yyyy-mm-dd', 0, 'fr', DateSystem.Date1904);
|
||||
});
|
||||
|
||||
it('applies and caches a legitimate empty number-format result', () => {
|
||||
const worksheet = workbook.getSheetBySheetId('sheet1')!;
|
||||
const previewSpy = vi.spyOn(patternUtils, 'getPatternPreviewIgnoreGeneral');
|
||||
|
||||
numfmtService.setValues('test', 'sheet1', [{ pattern: ';;;', ranges: [cellToRange(1, 1)] }]);
|
||||
|
||||
expect(getInterceptedCell(worksheet, workbook, 1, 1, get)).toMatchObject({
|
||||
v: '',
|
||||
t: CellValueType.NUMBER,
|
||||
coverable: false,
|
||||
});
|
||||
expect(worksheet.getCellRaw(1, 1)).toMatchObject({ v: '1234.5', t: CellValueType.STRING });
|
||||
|
||||
const previewCallsAfterFirstRender = previewSpy.mock.calls.length;
|
||||
getInterceptedCell(worksheet, workbook, 1, 1, get);
|
||||
expect(previewSpy).toHaveBeenCalledTimes(previewCallsAfterFirstRender);
|
||||
});
|
||||
|
||||
it('resets cached rendering when active sheet changes and supports locale override', () => {
|
||||
const sheet1 = workbook.getSheetBySheetId('sheet1')!;
|
||||
const sheet2 = workbook.getSheetBySheetId('sheet2')!;
|
||||
const previewSpy = vi.spyOn(patternUtils, 'getPatternPreviewIgnoreGeneral');
|
||||
|
||||
numfmtService.setValues('test', 'sheet1', [{ pattern: '0.00', ranges: [cellToRange(1, 1)] }]);
|
||||
|
||||
expect(getInterceptedCell(sheet1, workbook, 1, 1, get)).toMatchObject({ v: '1234.50', t: CellValueType.NUMBER });
|
||||
let previewCallCount = previewSpy.mock.calls.length;
|
||||
|
||||
getInterceptedCell(sheet1, workbook, 1, 1, get);
|
||||
expect(previewSpy).toHaveBeenCalledTimes(previewCallCount);
|
||||
|
||||
workbook.setActiveSheet(sheet2);
|
||||
|
||||
getInterceptedCell(sheet1, workbook, 1, 1, get);
|
||||
previewCallCount += 1;
|
||||
expect(previewSpy).toHaveBeenCalledTimes(previewCallCount);
|
||||
|
||||
workbook.setActiveSheet(sheet1);
|
||||
|
||||
getInterceptedCell(sheet1, workbook, 1, 1, get);
|
||||
previewCallCount += 1;
|
||||
expect(previewSpy).toHaveBeenCalledTimes(previewCallCount);
|
||||
|
||||
controller.setNumfmtLocal('fr');
|
||||
expect(controller.locale).toBe('fr');
|
||||
|
||||
getInterceptedCell(sheet1, workbook, 1, 1, get);
|
||||
previewCallCount += 1;
|
||||
expect(previewSpy).toHaveBeenCalledTimes(previewCallCount);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,219 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* 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 { ICellData, ICellDataForSheetInterceptor, INumfmtLocaleTag, Workbook } from '@univerjs/core';
|
||||
import type { ISetNumfmtMutationParams, ISetRangeValuesMutationParams } from '@univerjs/sheets';
|
||||
import type { IUniverSheetsNumfmtConfig } from '../config/config';
|
||||
import {
|
||||
CellValueType,
|
||||
Disposable,
|
||||
getNumfmtLocaleTag,
|
||||
getNumfmtParseValueFilter,
|
||||
ICommandService,
|
||||
IConfigService,
|
||||
Inject,
|
||||
InterceptorEffectEnum,
|
||||
isDefaultFormat,
|
||||
isTextFormat,
|
||||
IUniverInstanceService,
|
||||
LocaleService,
|
||||
ObjectMatrix,
|
||||
Range,
|
||||
ThemeService,
|
||||
UniverInstanceType,
|
||||
} from '@univerjs/core';
|
||||
import {
|
||||
checkCellValueType,
|
||||
InterceptCellContentPriority,
|
||||
INTERCEPTOR_POINT,
|
||||
INumfmtService,
|
||||
SetNumfmtMutation,
|
||||
SetRangeValuesMutation,
|
||||
SheetInterceptorService,
|
||||
} from '@univerjs/sheets';
|
||||
import { BehaviorSubject, merge, of, skip, switchMap } from 'rxjs';
|
||||
import { SHEETS_NUMFMT_PLUGIN_CONFIG_KEY } from '../config/config';
|
||||
import { getPatternPreviewIgnoreGeneral } from '../utils/pattern';
|
||||
|
||||
const TEXT_FORMAT_MARK = {
|
||||
tl: {
|
||||
size: 6,
|
||||
color: '#409f11',
|
||||
},
|
||||
};
|
||||
export class SheetsNumfmtCellContentController extends Disposable {
|
||||
private _locale$ = new BehaviorSubject<INumfmtLocaleTag | null>(null);
|
||||
public locale$ = this._locale$.asObservable();
|
||||
constructor(
|
||||
@IUniverInstanceService private readonly _instanceService: IUniverInstanceService,
|
||||
@Inject(SheetInterceptorService) private _sheetInterceptorService: SheetInterceptorService,
|
||||
@Inject(ThemeService) private _themeService: ThemeService,
|
||||
@Inject(ICommandService) private _commandService: ICommandService,
|
||||
@Inject(INumfmtService) private _numfmtService: INumfmtService,
|
||||
@Inject(LocaleService) private _localeService: LocaleService,
|
||||
@IConfigService private readonly _configService: IConfigService
|
||||
) {
|
||||
super();
|
||||
this._initInterceptorCellContent();
|
||||
}
|
||||
|
||||
public get locale(): INumfmtLocaleTag {
|
||||
return this.getLocale();
|
||||
}
|
||||
|
||||
public getLocale(workbook?: Workbook): INumfmtLocaleTag {
|
||||
return this._locale$.getValue() ?? getNumfmtLocaleTag(workbook?.getSnapshot().locale ?? this._localeService.getCurrentLocale());
|
||||
}
|
||||
|
||||
// eslint-disable-next-line max-lines-per-function
|
||||
private _initInterceptorCellContent() {
|
||||
const renderCache = new ObjectMatrix<{ result: ICellData; parameters: string | number }>();
|
||||
|
||||
this.disposeWithMe(merge(this._locale$, this._localeService.currentLocale$).subscribe(() => {
|
||||
renderCache.reset();
|
||||
}));
|
||||
|
||||
this.disposeWithMe(this._sheetInterceptorService.intercept(INTERCEPTOR_POINT.CELL_CONTENT, {
|
||||
effect: InterceptorEffectEnum.Value | InterceptorEffectEnum.Style,
|
||||
|
||||
// eslint-disable-next-line max-lines-per-function, complexity
|
||||
handler: (cell, location, next) => {
|
||||
// If the cell is empty, or the cell type is boolean or force_string, do not process it.
|
||||
// Cell type is number or number string, it will be applied number format.
|
||||
if (!cell || cell.v === undefined || cell.v === null || cell.t === CellValueType.BOOLEAN || cell.t === CellValueType.FORCE_STRING) {
|
||||
return next(cell);
|
||||
}
|
||||
|
||||
const unitId = location.unitId;
|
||||
const sheetId = location.subUnitId;
|
||||
const locale = this.getLocale(location.workbook);
|
||||
const dateSystem = location.workbook.getDateSystem();
|
||||
let numfmtValue;
|
||||
|
||||
if (cell?.s) {
|
||||
const style = location.workbook.getStyles().get(cell.s);
|
||||
if (style?.n) {
|
||||
numfmtValue = style.n;
|
||||
}
|
||||
}
|
||||
|
||||
if (!numfmtValue) {
|
||||
numfmtValue = this._numfmtService.getValue(unitId, sheetId, location.row, location.col);
|
||||
}
|
||||
|
||||
// If the cell is not formatted, or the format is 'General', do not process it
|
||||
// e.g. { v: '001', t: 1, s: { n: { pattern: 'General' } } } should display as '001'
|
||||
if (isDefaultFormat(numfmtValue?.pattern)) {
|
||||
return next(cell);
|
||||
}
|
||||
|
||||
// If the cell not specified number type, then check the cell value type
|
||||
if (cell.t !== CellValueType.NUMBER) {
|
||||
const type = checkCellValueType(cell.v, cell.t);
|
||||
// just handle number/number string/number string with text format, other type will not be processed
|
||||
if (
|
||||
type !== CellValueType.NUMBER &&
|
||||
!(
|
||||
isTextFormat(numfmtValue?.pattern) &&
|
||||
typeof cell.v === 'string' &&
|
||||
typeof getNumfmtParseValueFilter(cell.v, { locale, dateSystem })?.v === 'number'
|
||||
)
|
||||
) {
|
||||
return next(cell);
|
||||
}
|
||||
}
|
||||
|
||||
const originCellValue = cell;
|
||||
if (!cell || cell === location.rawData) {
|
||||
cell = { ...location.rawData };
|
||||
}
|
||||
|
||||
// Add error marker to text format number
|
||||
if (isTextFormat(numfmtValue?.pattern)) {
|
||||
// If the user has disabled the text format mark, do not show it
|
||||
if (this._configService.getConfig<IUniverSheetsNumfmtConfig>(SHEETS_NUMFMT_PLUGIN_CONFIG_KEY)?.disableTextFormatMark) {
|
||||
cell.t = CellValueType.STRING;
|
||||
return next(cell);
|
||||
}
|
||||
|
||||
cell.t = CellValueType.STRING;
|
||||
cell.markers = { ...cell?.markers, ...TEXT_FORMAT_MARK };
|
||||
return next(cell);
|
||||
}
|
||||
|
||||
const cacheParameters = `${unitId}_${sheetId}_${originCellValue.v}_${numfmtValue?.pattern}_${locale}_${dateSystem}`;
|
||||
const cache = renderCache.getValue(location.row, location.col);
|
||||
if (cache && cache.parameters === cacheParameters) {
|
||||
return next({ ...cell, ...cache.result });
|
||||
}
|
||||
|
||||
const info = getPatternPreviewIgnoreGeneral(numfmtValue?.pattern as string, Number(originCellValue.v), locale, dateSystem);
|
||||
const numfmtRes = info.result;
|
||||
const res: ICellDataForSheetInterceptor = { v: numfmtRes, t: CellValueType.NUMBER };
|
||||
if (numfmtRes === '') {
|
||||
res.coverable = false;
|
||||
}
|
||||
if (info.color) {
|
||||
const color = this._themeService.getColorFromTheme(`${info.color}.500`) ?? info.color;
|
||||
|
||||
if (color) {
|
||||
res.interceptorStyle = { cl: { rgb: color } };
|
||||
}
|
||||
}
|
||||
|
||||
renderCache.setValue(location.row, location.col, {
|
||||
result: res,
|
||||
parameters: cacheParameters,
|
||||
});
|
||||
Object.assign(cell, res);
|
||||
return next(cell);
|
||||
},
|
||||
priority: InterceptCellContentPriority.NUMFMT,
|
||||
}));
|
||||
|
||||
this.disposeWithMe(this._commandService.onCommandExecuted((commandInfo) => {
|
||||
if (commandInfo.id === SetNumfmtMutation.id) {
|
||||
const params = commandInfo.params as ISetNumfmtMutationParams;
|
||||
Object.keys(params.values).forEach((key) => {
|
||||
const v = params.values[key];
|
||||
v.ranges.forEach((range) => {
|
||||
Range.foreach(range, (row, col) => {
|
||||
renderCache.realDeleteValue(row, col);
|
||||
});
|
||||
});
|
||||
});
|
||||
} else if (commandInfo.id === SetRangeValuesMutation.id) {
|
||||
const params = commandInfo.params as ISetRangeValuesMutationParams;
|
||||
new ObjectMatrix(params.cellValue).forValue((row, col) => {
|
||||
renderCache.realDeleteValue(row, col);
|
||||
});
|
||||
}
|
||||
}));
|
||||
|
||||
this.disposeWithMe(
|
||||
this._instanceService.getCurrentTypeOfUnit$<Workbook>(UniverInstanceType.UNIVER_SHEET)
|
||||
.pipe(
|
||||
switchMap((workbook) => workbook?.activeSheet$ ?? of(null)),
|
||||
skip(1)
|
||||
)
|
||||
.subscribe(() => renderCache.reset())
|
||||
);
|
||||
}
|
||||
|
||||
setNumfmtLocal(locale: INumfmtLocaleTag) {
|
||||
this._locale$.next(locale);
|
||||
}
|
||||
}
|
||||
22
packages/sheets-numfmt/src/controllers/type.ts
Normal file
22
packages/sheets-numfmt/src/controllers/type.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* 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 { createIdentifier } from '@univerjs/core';
|
||||
|
||||
export const INumfmtController = createIdentifier<INumfmtController>('INumfmtController');
|
||||
export interface INumfmtController {
|
||||
openPanel(): void;
|
||||
}
|
||||
159
packages/sheets-numfmt/src/facade/__tests__/create-test-bed.ts
Normal file
159
packages/sheets-numfmt/src/facade/__tests__/create-test-bed.ts
Normal file
@@ -0,0 +1,159 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* 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 { Dependency, IWorkbookData, UnitModel } from '@univerjs/core';
|
||||
import {
|
||||
ILogService,
|
||||
Inject,
|
||||
Injector,
|
||||
IUniverInstanceService,
|
||||
LocaleService,
|
||||
LocaleType,
|
||||
LogLevel,
|
||||
Plugin,
|
||||
set,
|
||||
ThemeService,
|
||||
Univer,
|
||||
UniverInstanceType,
|
||||
} from '@univerjs/core';
|
||||
import { FUniver } from '@univerjs/core/facade';
|
||||
import { DefinedNamesService, FormulaDataModel, IDefinedNamesService, LexerTreeBuilder } from '@univerjs/engine-formula';
|
||||
import { INumfmtService, NumfmtService, SheetInterceptorService, SheetsSelectionsService } from '@univerjs/sheets';
|
||||
|
||||
import enUS from '@univerjs/sheets/locale/en-US';
|
||||
import zhCN from '@univerjs/sheets/locale/zh-CN';
|
||||
|
||||
import '@univerjs/sheets/facade';
|
||||
|
||||
function getTestWorkbookDataDemo(): IWorkbookData {
|
||||
return {
|
||||
id: 'test',
|
||||
appVersion: '3.0.0-alpha',
|
||||
sheets: {
|
||||
sheet1: {
|
||||
id: 'sheet1',
|
||||
name: 'sheet1',
|
||||
cellData: {
|
||||
0: {
|
||||
3: {
|
||||
f: '=SUM(A1)',
|
||||
si: '3e4r5t',
|
||||
},
|
||||
4: {
|
||||
v: 123,
|
||||
t: 2,
|
||||
},
|
||||
},
|
||||
1: {
|
||||
3: {
|
||||
f: '=SUM(A2)',
|
||||
si: 'OSPtzm',
|
||||
},
|
||||
},
|
||||
2: {
|
||||
3: {
|
||||
si: 'OSPtzm',
|
||||
},
|
||||
},
|
||||
3: {
|
||||
3: {
|
||||
si: 'OSPtzm',
|
||||
},
|
||||
},
|
||||
},
|
||||
rowCount: 100,
|
||||
columnCount: 100,
|
||||
},
|
||||
},
|
||||
locale: LocaleType.ZH_CN,
|
||||
name: '',
|
||||
sheetOrder: [],
|
||||
styles: {},
|
||||
};
|
||||
}
|
||||
|
||||
export interface ITestBed {
|
||||
univer: Univer;
|
||||
get: Injector['get'];
|
||||
sheet: UnitModel<IWorkbookData>;
|
||||
univerAPI: FUniver;
|
||||
injector: Injector;
|
||||
}
|
||||
|
||||
export function createFacadeTestBed(workbookData?: IWorkbookData, dependencies?: Dependency[]): ITestBed {
|
||||
const univer = new Univer();
|
||||
const injector = univer.__getInjector();
|
||||
|
||||
class TestPlugin extends Plugin {
|
||||
static override pluginName = 'test-plugin';
|
||||
static override type = UniverInstanceType.UNIVER_SHEET;
|
||||
|
||||
constructor(
|
||||
_config: undefined,
|
||||
@Inject(Injector) override readonly _injector: Injector
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
override onStarting(): void {
|
||||
injector.add([SheetsSelectionsService]);
|
||||
injector.add([SheetInterceptorService]);
|
||||
injector.add([IDefinedNamesService, { useClass: DefinedNamesService }]);
|
||||
injector.add([FormulaDataModel]);
|
||||
injector.add([LexerTreeBuilder]);
|
||||
injector.add([INumfmtService, { useClass: NumfmtService }]);
|
||||
dependencies?.forEach((d) => injector.add(d));
|
||||
|
||||
this._injector.get(SheetInterceptorService);
|
||||
}
|
||||
|
||||
override onReady(): void {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// load i18n
|
||||
injector.get(LocaleService).load({ zhCN, enUS });
|
||||
|
||||
// load theme service
|
||||
const themeService = injector.get(ThemeService);
|
||||
const theme = themeService.getCurrentTheme();
|
||||
const newTheme = set(theme, 'gray.1000', '#35322b');
|
||||
themeService.setTheme(newTheme);
|
||||
|
||||
// register builtin plugins
|
||||
// note that UI plugins are not registered here, because the unit test environment does not have a UI
|
||||
univer.registerPlugin(TestPlugin);
|
||||
|
||||
const sheet = univer.createUnit<IWorkbookData, UnitModel<IWorkbookData>>(UniverInstanceType.UNIVER_SHEET, workbookData || getTestWorkbookDataDemo());
|
||||
const univerInstanceService = injector.get(IUniverInstanceService);
|
||||
univerInstanceService.focusUnit('test');
|
||||
|
||||
// set log level
|
||||
const logService = injector.get(ILogService);
|
||||
logService.setLogLevel(LogLevel.SILENT); // NOTE: change this to `LogLevel.VERBOSE` to debug tests via logs
|
||||
|
||||
// init data validation
|
||||
const univerAPI = FUniver.newAPI(injector);
|
||||
|
||||
return {
|
||||
univer,
|
||||
get: injector.get.bind(injector),
|
||||
sheet,
|
||||
univerAPI,
|
||||
injector,
|
||||
};
|
||||
}
|
||||
78
packages/sheets-numfmt/src/facade/__tests__/f-range.spec.ts
Normal file
78
packages/sheets-numfmt/src/facade/__tests__/f-range.spec.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* 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 { FUniver } from '@univerjs/core/facade';
|
||||
import { ICommandService } from '@univerjs/core';
|
||||
import { RemoveNumfmtMutation, SetNumfmtMutation, SetRangeValuesCommand, SetRangeValuesMutation } from '@univerjs/sheets';
|
||||
import { beforeEach, describe, expect, it } from 'vitest';
|
||||
import { SetNumfmtCommand, SheetsNumfmtCellContentController } from '../../index';
|
||||
import { createFacadeTestBed } from './create-test-bed';
|
||||
import '../index';
|
||||
|
||||
describe('Test FRange', () => {
|
||||
let univerAPI: FUniver;
|
||||
let testBed: ReturnType<typeof createFacadeTestBed>;
|
||||
|
||||
beforeEach(() => {
|
||||
testBed = createFacadeTestBed(undefined, [[SheetsNumfmtCellContentController]]);
|
||||
const commandService = testBed.injector.get(ICommandService);
|
||||
[
|
||||
SetRangeValuesMutation,
|
||||
SetRangeValuesCommand,
|
||||
SetNumfmtMutation,
|
||||
RemoveNumfmtMutation,
|
||||
SetNumfmtCommand,
|
||||
].forEach((command) => commandService.registerCommand(command));
|
||||
testBed.injector.get(SheetsNumfmtCellContentController);
|
||||
univerAPI = testBed.univerAPI;
|
||||
});
|
||||
|
||||
it('sets and reads number formats on a single range and a range grid', () => {
|
||||
const activeSheet = univerAPI.getActiveWorkbook()!.getActiveSheet();
|
||||
const singleCell = activeSheet.getRange('A1');
|
||||
|
||||
expect(singleCell.setNumberFormat('#,##0.00')).toBe(singleCell);
|
||||
expect(singleCell.getNumberFormat()).toBe('#,##0.00');
|
||||
|
||||
const range = activeSheet.getRange('A1:B2');
|
||||
range.setNumberFormats([
|
||||
['#,##0.00', '0.00%'],
|
||||
['yyyy-MM-DD', ''],
|
||||
]);
|
||||
|
||||
expect(range.getNumberFormats()).toEqual([
|
||||
['#,##0.00', '0.00%'],
|
||||
['yyyy-MM-DD', ''],
|
||||
]);
|
||||
});
|
||||
|
||||
it('sets workbook number format locale through the facade API', () => {
|
||||
const workbook = univerAPI.getActiveWorkbook()!;
|
||||
expect(workbook.setNumfmtLocal('fr')).toBe(workbook);
|
||||
});
|
||||
|
||||
it('hides a formatted value without changing its raw value', () => {
|
||||
const activeSheet = univerAPI.getActiveWorkbook()!.getActiveSheet();
|
||||
const cell = activeSheet.getRange('A1');
|
||||
|
||||
cell.setValue(233628);
|
||||
cell.setNumberFormat(';;;');
|
||||
|
||||
expect(cell.getDisplayValue()).toBe('');
|
||||
expect(cell.getRawValue()).toBe(233628);
|
||||
expect(cell.getNumberFormat()).toBe(';;;');
|
||||
});
|
||||
});
|
||||
159
packages/sheets-numfmt/src/facade/f-range.ts
Normal file
159
packages/sheets-numfmt/src/facade/f-range.ts
Normal file
@@ -0,0 +1,159 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* 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 { ISetNumfmtCommandParams } from '@univerjs/sheets-numfmt';
|
||||
import { SetNumfmtCommand } from '@univerjs/sheets-numfmt';
|
||||
import { FRange } from '@univerjs/sheets/facade';
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
export interface IFRangeSheetsNumfmtMixin {
|
||||
// TODO@wzhudev: should separate numfmt package to two
|
||||
|
||||
/**
|
||||
* Set the number format of the range.
|
||||
* @param {string} pattern - The number format pattern.
|
||||
* @returns {FRange} The FRange instance for chaining.
|
||||
* @example
|
||||
* ```ts
|
||||
* const fWorkbook = univerAPI.getActiveWorkbook();
|
||||
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
|
||||
* if (!fWorksheet) return;
|
||||
*
|
||||
* // Set the number format of the A1 cell to '#,##0.00'.
|
||||
* const fRange = fWorksheet.getRange('A1');
|
||||
* fRange.setValue(1234.567).setNumberFormat('#,##0.00');
|
||||
* console.log(fRange.getDisplayValue()); // 1,234.57
|
||||
* ```
|
||||
*/
|
||||
setNumberFormat(pattern: string): FRange;
|
||||
|
||||
/**
|
||||
* Sets a rectangular grid of number formats (must match dimensions of this range).
|
||||
* @param {string[][]} patterns - A two-dimensional array of number formats.
|
||||
* @returns {FRange} The FRange instance for chaining.
|
||||
* @example
|
||||
* ```ts
|
||||
* const fWorkbook = univerAPI.getActiveWorkbook();
|
||||
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
|
||||
* if (!fWorksheet) return;
|
||||
*
|
||||
* // Set the number formats of the A1:B2 range.
|
||||
* const fRange = fWorksheet.getRange('A1:B2');
|
||||
* fRange.setValues([
|
||||
* [1234.567, 0.1234],
|
||||
* [45658, 0.9876]
|
||||
* ]).setNumberFormats([
|
||||
* ['#,##0.00', '0.00%'],
|
||||
* ['yyyy-MM-DD', '']
|
||||
* ]);
|
||||
* console.log(fRange.getDisplayValues()); // [['1,234.57', '12.34%'], ['2025-01-01', '0.9876']]
|
||||
* ```
|
||||
*/
|
||||
setNumberFormats(patterns: string[][]): FRange;
|
||||
|
||||
/**
|
||||
* Get the number formatting of the top-left cell of the given range. Returns an empty string when no number format is set, even if the cell contains a value.
|
||||
* @returns {string} The number format of the top-left cell of the range.
|
||||
* @example
|
||||
* ```ts
|
||||
* const fWorkbook = univerAPI.getActiveWorkbook();
|
||||
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
|
||||
* if (!fWorksheet) return;
|
||||
*
|
||||
* // Get the number format of the top-left cell of the A1:B2 range.
|
||||
* const fRange = fWorksheet.getRange('A1:B2');
|
||||
* console.log(fRange.getNumberFormat());
|
||||
* ```
|
||||
*/
|
||||
getNumberFormat(): string;
|
||||
|
||||
/**
|
||||
* Returns the number formats for the cells in the range.
|
||||
* @returns {string[][]} A two-dimensional array of number formats.
|
||||
* @example
|
||||
* ```ts
|
||||
* const fWorkbook = univerAPI.getActiveWorkbook();
|
||||
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
|
||||
* if (!fWorksheet) return;
|
||||
*
|
||||
* // Get the number formats of the A1:B2 range.
|
||||
* const fRange = fWorksheet.getRange('A1:B2');
|
||||
* console.log(fRange.getNumberFormats());
|
||||
* ```
|
||||
*/
|
||||
getNumberFormats(): string[][];
|
||||
}
|
||||
|
||||
export class FRangeSheetsNumfmtMixin extends FRange implements IFRangeSheetsNumfmtMixin {
|
||||
override setNumberFormat(pattern: string): FRange {
|
||||
// TODO@Gggpound: the API should support other types of parameters
|
||||
const values: ISetNumfmtCommandParams['values'] = [];
|
||||
|
||||
// Add number format info to the `values` array.
|
||||
const { startColumn, startRow, endColumn, endRow } = this._range;
|
||||
for (let row = startRow; row <= endRow; row++) {
|
||||
for (let col = startColumn; col <= endColumn; col++) {
|
||||
values.push({ row, col, pattern });
|
||||
}
|
||||
}
|
||||
|
||||
this._commandService.syncExecuteCommand(SetNumfmtCommand.id, {
|
||||
unitId: this._workbook.getUnitId(),
|
||||
subUnitId: this._worksheet.getSheetId(),
|
||||
values,
|
||||
} as ISetNumfmtCommandParams);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
override setNumberFormats(patterns: string[][]): FRange {
|
||||
const values: ISetNumfmtCommandParams['values'] = [];
|
||||
|
||||
// Add number format info to the `values` array.
|
||||
const { startColumn, startRow, endColumn, endRow } = this._range;
|
||||
for (let row = startRow; row <= endRow; row++) {
|
||||
for (let col = startColumn; col <= endColumn; col++) {
|
||||
const pattern = patterns[row - startRow]?.[col - startColumn];
|
||||
values.push({ row, col, pattern });
|
||||
}
|
||||
}
|
||||
|
||||
this._commandService.syncExecuteCommand(SetNumfmtCommand.id, {
|
||||
unitId: this._workbook.getUnitId(),
|
||||
subUnitId: this._worksheet.getSheetId(),
|
||||
values,
|
||||
} as ISetNumfmtCommandParams);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
override getNumberFormat(): string {
|
||||
const style = this.getCellStyle();
|
||||
return style?.numberFormat?.pattern ?? '';
|
||||
}
|
||||
|
||||
override getNumberFormats(): string[][] {
|
||||
const styles = this.getCellStyles();
|
||||
return styles.map((row) => row.map((cellStyle) => cellStyle?.numberFormat?.pattern ?? ''));
|
||||
}
|
||||
}
|
||||
|
||||
FRange.extend(FRangeSheetsNumfmtMixin);
|
||||
declare module '@univerjs/sheets/facade' {
|
||||
interface FRange extends IFRangeSheetsNumfmtMixin { }
|
||||
}
|
||||
57
packages/sheets-numfmt/src/facade/f-workbook.ts
Normal file
57
packages/sheets-numfmt/src/facade/f-workbook.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* 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 { INumfmtLocaleTag } from '@univerjs/core';
|
||||
import { SheetsNumfmtCellContentController } from '@univerjs/sheets-numfmt';
|
||||
import { FWorkbook } from '@univerjs/sheets/facade';
|
||||
|
||||
export interface IFWorkbookSheetsNumfmtMixin {
|
||||
/**
|
||||
* Set the locale for number formatting.
|
||||
* @param {INumfmtLocaleTag} locale zh_CN,zh_TW,zh_HK,ja,ko,th,cs,da,nl,en,en_AU,en_CA,en_GB,en_IE,fi,fr,fr_CA,fr_CH,de,de_CH,el,hu,is,id,it,it_CH,nb,no,pl,pt,pt_BR,ru,sk,es,es_AR,es_BO,es_CL,es_CO,es_EC,es_MX,es_PY,es_UY,es_VE,sv,tr,cy,az,be,bg,ca,fil,gu,he,hr,hy,ka,kk,kn,lt,lv,ml,mn,mr,my,pa,ro,sl,sr,ta,te,uk,vi,ar,bn,hi
|
||||
* @returns {FWorkbook} The FWorkbook instance for chaining.
|
||||
* @memberof IFWorkbookNumfmtMixin
|
||||
* @example
|
||||
* ```ts
|
||||
* const fWorkbook = univerAPI.getActiveWorkbook();
|
||||
* const fWorksheet = fWorkbook.getSheetByName('Sheet1');
|
||||
* if (!fWorksheet) return;
|
||||
* const fRange = fWorksheet.getRange('A1');
|
||||
* fRange.setValue(1234.567).setNumberFormat('#,##0.00');
|
||||
*
|
||||
* // Set the locale en_US for number formatting.
|
||||
* fWorkbook.setNumfmtLocal('en_US');
|
||||
* console.log(fRange.getDisplayValue()); // 1,234.57
|
||||
*
|
||||
* // Set the locale de_DE for number formatting.
|
||||
* fWorkbook.setNumfmtLocal('de_DE');
|
||||
* console.log(fRange.getDisplayValue()); // 1.234,57
|
||||
* ```
|
||||
*/
|
||||
setNumfmtLocal(locale: INumfmtLocaleTag): FWorkbook;
|
||||
}
|
||||
export class FWorkbookSheetsNumfmtMixin extends FWorkbook implements IFWorkbookSheetsNumfmtMixin {
|
||||
override setNumfmtLocal(locale: INumfmtLocaleTag): FWorkbook {
|
||||
const sheetsNumfmtCellContentController = this._injector.get(SheetsNumfmtCellContentController);
|
||||
sheetsNumfmtCellContentController.setNumfmtLocal(locale);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
FWorkbook.extend(FWorkbookSheetsNumfmtMixin);
|
||||
declare module '@univerjs/sheets/facade' {
|
||||
// eslint-disable-next-line ts/naming-convention
|
||||
interface FWorkbook extends IFWorkbookSheetsNumfmtMixin { }
|
||||
}
|
||||
21
packages/sheets-numfmt/src/facade/index.ts
Normal file
21
packages/sheets-numfmt/src/facade/index.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* 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 './f-range';
|
||||
import './f-workbook';
|
||||
|
||||
export type * from './f-range';
|
||||
export type * from './f-workbook';
|
||||
42
packages/sheets-numfmt/src/index.ts
Normal file
42
packages/sheets-numfmt/src/index.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* 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 {
|
||||
getCurrencyFormat,
|
||||
getCurrencySymbolByLocale,
|
||||
getCurrencySymbolIconByLocale,
|
||||
localeCurrencySymbolMap,
|
||||
} from './base/const/currency-symbols';
|
||||
export { CURRENCYFORMAT, DATEFMTLISG, NUMBERFORMAT } from './base/const/formatdetail';
|
||||
export { AddDecimalCommand } from './commands/commands/add-decimal.command';
|
||||
export { SetCurrencyCommand } from './commands/commands/set-currency.command';
|
||||
export { SetNumfmtCommand } from './commands/commands/set-numfmt.command';
|
||||
export type { ISetNumfmtCommandParams } from './commands/commands/set-numfmt.command';
|
||||
export { SetPercentCommand } from './commands/commands/set-percent.command';
|
||||
export { SubtractDecimalCommand } from './commands/commands/subtract-decimal.command';
|
||||
export { SHEETS_NUMFMT_PLUGIN_CONFIG_KEY } from './config/config';
|
||||
export type { IUniverSheetsNumfmtConfig } from './config/config';
|
||||
export { SheetsNumfmtCellContentController } from './controllers/numfmt-cell-content.controller';
|
||||
export { UniverSheetsNumfmtPlugin } from './plugin';
|
||||
export { getCurrencyType } from './utils/currency';
|
||||
export { getDecimalFromPattern, getDecimalString, isPatternHasDecimal, setPatternDecimal } from './utils/decimal';
|
||||
export {
|
||||
getCurrencyFormatOptions,
|
||||
getCurrencyOptions,
|
||||
getDateFormatOptions,
|
||||
getNumberFormatOptions,
|
||||
} from './utils/options';
|
||||
export { getPatternPreview, getPatternPreviewIgnoreGeneral, getPatternType } from './utils/pattern';
|
||||
74
packages/sheets-numfmt/src/plugin.ts
Normal file
74
packages/sheets-numfmt/src/plugin.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* 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 { IUniverSheetsNumfmtConfig } from './config/config';
|
||||
import { DependentOn, ICommandService, IConfigService, Inject, Injector, merge, Plugin, registerDependencies, touchDependencies, UniverInstanceType } from '@univerjs/core';
|
||||
import { UniverSheetsPlugin } from '@univerjs/sheets';
|
||||
import pkg from '../package.json';
|
||||
import { SHEET_NUMFMT_PLUGIN } from './base/const/plugin-name';
|
||||
import { AddDecimalCommand } from './commands/commands/add-decimal.command';
|
||||
import { SetCurrencyCommand } from './commands/commands/set-currency.command';
|
||||
import { SetNumfmtCommand } from './commands/commands/set-numfmt.command';
|
||||
import { SetPercentCommand } from './commands/commands/set-percent.command';
|
||||
import { SubtractDecimalCommand } from './commands/commands/subtract-decimal.command';
|
||||
import { defaultPluginConfig, SHEETS_NUMFMT_PLUGIN_CONFIG_KEY } from './config/config';
|
||||
import { SheetsNumfmtCellContentController } from './controllers/numfmt-cell-content.controller';
|
||||
|
||||
@DependentOn(UniverSheetsPlugin)
|
||||
export class UniverSheetsNumfmtPlugin extends Plugin {
|
||||
static override pluginName = SHEET_NUMFMT_PLUGIN;
|
||||
static override packageName = pkg.name;
|
||||
static override version = pkg.version;
|
||||
static override type = UniverInstanceType.UNIVER_SHEET;
|
||||
|
||||
constructor(
|
||||
private readonly _config: Partial<IUniverSheetsNumfmtConfig> = defaultPluginConfig,
|
||||
@Inject(Injector) override readonly _injector: Injector,
|
||||
@IConfigService private readonly _configService: IConfigService,
|
||||
@ICommandService private readonly _commandService: ICommandService
|
||||
) {
|
||||
super();
|
||||
|
||||
// Manage the plugin configuration.
|
||||
const { ...rest } = merge(
|
||||
{},
|
||||
defaultPluginConfig,
|
||||
this._config
|
||||
);
|
||||
|
||||
this._configService.setConfig(SHEETS_NUMFMT_PLUGIN_CONFIG_KEY, rest);
|
||||
}
|
||||
|
||||
override onStarting(): void {
|
||||
registerDependencies(this._injector, [
|
||||
[SheetsNumfmtCellContentController],
|
||||
]);
|
||||
|
||||
touchDependencies(this._injector, [
|
||||
[SheetsNumfmtCellContentController],
|
||||
]);
|
||||
|
||||
[
|
||||
AddDecimalCommand,
|
||||
SubtractDecimalCommand,
|
||||
SetCurrencyCommand,
|
||||
SetPercentCommand,
|
||||
SetNumfmtCommand,
|
||||
].forEach((config) => {
|
||||
this.disposeWithMe(this._commandService.registerCommand(config));
|
||||
});
|
||||
}
|
||||
}
|
||||
42
packages/sheets-numfmt/src/utils/__tests__/pattern.spec.ts
Normal file
42
packages/sheets-numfmt/src/utils/__tests__/pattern.spec.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* 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 { DEFAULT_NUMBER_FORMAT } from '@univerjs/core';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { getPatternPreview, getPatternType } from '../pattern';
|
||||
|
||||
describe('getPatternType', () => {
|
||||
it('classifies conditional general formats as number formats', () => {
|
||||
expect(getPatternType(DEFAULT_NUMBER_FORMAT)).toBe('general');
|
||||
expect(getPatternType('[>0]"A";[<0]"B";"B"')).toBe('number');
|
||||
});
|
||||
|
||||
it('classifies two-section conditional text formats as number formats', () => {
|
||||
const pattern = '[>0]"未完成";[<=0]"完成"';
|
||||
expect(getPatternType(pattern)).toBe('number');
|
||||
expect(getPatternPreview(pattern, 1)).toEqual({ result: '未完成' });
|
||||
expect(getPatternPreview(pattern, 0)).toEqual({ result: '完成' });
|
||||
expect(getPatternPreview(pattern, -1)).toEqual({ result: '-完成' });
|
||||
});
|
||||
|
||||
it('classifies three-section conditional text formats as number formats', () => {
|
||||
const pattern = '[>0]"未完成";[<0]"完成";"完成"';
|
||||
expect(getPatternType(pattern)).toBe('number');
|
||||
expect(getPatternPreview(pattern, 1)).toEqual({ result: '未完成' });
|
||||
expect(getPatternPreview(pattern, 0)).toEqual({ result: '完成' });
|
||||
expect(getPatternPreview(pattern, -1)).toEqual({ result: '完成' });
|
||||
});
|
||||
});
|
||||
22
packages/sheets-numfmt/src/utils/currency.ts
Normal file
22
packages/sheets-numfmt/src/utils/currency.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* 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 { currencySymbols } from '@univerjs/core';
|
||||
|
||||
export const getCurrencyType = (pattern: string) => {
|
||||
const item = currencySymbols.find((code) => pattern.includes(code));
|
||||
return item;
|
||||
};
|
||||
54
packages/sheets-numfmt/src/utils/decimal.ts
Normal file
54
packages/sheets-numfmt/src/utils/decimal.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* 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 { numfmt } from '@univerjs/core';
|
||||
|
||||
/**
|
||||
* the function decimal just use positive,negative configuration ignored
|
||||
*/
|
||||
export const getDecimalFromPattern = (pattern: string, defaultValue: number = 0) => {
|
||||
if (!pattern) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
const info = numfmt.getFormatInfo(pattern);
|
||||
return info.maxDecimals ?? defaultValue;
|
||||
};
|
||||
|
||||
export const getDecimalString = (length: number) =>
|
||||
new Array(Math.min(Math.max(0, Number(length)), 30)).fill(0).join('');
|
||||
|
||||
export const setPatternDecimal = (patterns: string, decimalLength: number) => {
|
||||
const tokens = patterns.split(';').map((pattern) => {
|
||||
if (/\.0?/.test(pattern)) {
|
||||
return pattern.replace(
|
||||
/\.0*/g,
|
||||
`${decimalLength > 0 ? '.' : ''}${getDecimalString(Number(decimalLength || 0))}`
|
||||
);
|
||||
}
|
||||
if (/0([^0]?)|0$/.test(pattern)) {
|
||||
return pattern.replace(
|
||||
/0([^0]+)|0$/,
|
||||
`0${decimalLength > 0 ? '.' : ''}${getDecimalString(Number(decimalLength || 0))}$1`
|
||||
);
|
||||
}
|
||||
|
||||
return pattern;
|
||||
});
|
||||
return tokens.join(';');
|
||||
};
|
||||
|
||||
export const isPatternHasDecimal = (pattern: string) => /\.0?/.test(pattern) || /0([^0]?)|0$/.test(pattern);
|
||||
91
packages/sheets-numfmt/src/utils/mutation.ts
Normal file
91
packages/sheets-numfmt/src/utils/mutation.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* 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 { IMutationInfo } from '@univerjs/core';
|
||||
import type { IRemoveNumfmtMutationParams, ISetNumfmtMutationParams } from '@univerjs/sheets';
|
||||
import { Tools } from '@univerjs/core';
|
||||
import { rangeMerge, RemoveNumfmtMutation, SetNumfmtMutation } from '@univerjs/sheets';
|
||||
|
||||
export const mergeNumfmtMutations = (list: IMutationInfo[]) => {
|
||||
const removeMutation = list
|
||||
.filter((item) => item.id === RemoveNumfmtMutation.id)
|
||||
.map((item) => item.params) as unknown as IRemoveNumfmtMutationParams[];
|
||||
const setMutation = list
|
||||
.filter((item) => item.id === SetNumfmtMutation.id)
|
||||
.map((item) => item.params) as unknown as ISetNumfmtMutationParams[];
|
||||
const result: IMutationInfo[] = [];
|
||||
|
||||
if (removeMutation[0]) {
|
||||
const params = removeMutation.reduce(
|
||||
(res, cur) => {
|
||||
res.ranges.push(...cur.ranges);
|
||||
return res;
|
||||
},
|
||||
{
|
||||
ranges: [],
|
||||
unitId: removeMutation[0].unitId,
|
||||
subUnitId: removeMutation[0].subUnitId,
|
||||
} as IRemoveNumfmtMutationParams
|
||||
);
|
||||
params.ranges = rangeMerge(params.ranges);
|
||||
result.push({ id: RemoveNumfmtMutation.id, params });
|
||||
}
|
||||
const findKeyFromObj = (obj: Record<string, any>, item: any) => {
|
||||
for (const key in obj) {
|
||||
if (!Object.prototype.hasOwnProperty.call(obj, key)) {
|
||||
continue;
|
||||
}
|
||||
const value = obj[key];
|
||||
if (Tools.diffValue(value, item)) {
|
||||
return key;
|
||||
}
|
||||
}
|
||||
};
|
||||
if (setMutation[0]) {
|
||||
let nextIndex = 1;
|
||||
const params = setMutation.reduce(
|
||||
(res, cur) => {
|
||||
Object.keys(cur.values).forEach((key) => {
|
||||
const curValue = cur.values[key];
|
||||
const curRef = cur.refMap[key];
|
||||
const index = findKeyFromObj(res.refMap, curRef);
|
||||
if (index) {
|
||||
res.values[index].ranges.push(...curValue.ranges);
|
||||
} else {
|
||||
const newIndex = nextIndex++;
|
||||
res.values[newIndex] = {
|
||||
ranges: curValue.ranges,
|
||||
};
|
||||
res.refMap[newIndex] = curRef;
|
||||
}
|
||||
});
|
||||
return res;
|
||||
},
|
||||
{
|
||||
values: {},
|
||||
refMap: {},
|
||||
unitId: setMutation[0].unitId,
|
||||
subUnitId: setMutation[0].subUnitId,
|
||||
} as ISetNumfmtMutationParams
|
||||
);
|
||||
Object.keys(params.values).forEach((key) => {
|
||||
const v = params.values[key];
|
||||
v.ranges = rangeMerge(v.ranges);
|
||||
});
|
||||
result.push({ id: SetNumfmtMutation.id, params });
|
||||
}
|
||||
return result;
|
||||
};
|
||||
32
packages/sheets-numfmt/src/utils/options.ts
Normal file
32
packages/sheets-numfmt/src/utils/options.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* 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 { currencySymbols } from '@univerjs/core';
|
||||
import { CURRENCYFORMAT, DATEFMTLISG, NUMBERFORMAT } from '../base/const/formatdetail';
|
||||
|
||||
export const getCurrencyOptions = () => currencySymbols.map((item) => ({ label: item, value: item }));
|
||||
|
||||
export const getCurrencyFormatOptions = (suffix: string) =>
|
||||
CURRENCYFORMAT.map((item) => ({
|
||||
label: item.label(suffix),
|
||||
value: item.suffix(suffix),
|
||||
color: item.color,
|
||||
}));
|
||||
|
||||
export const getDateFormatOptions = () => DATEFMTLISG.map((item) => ({ label: item.label, value: item.suffix }));
|
||||
|
||||
export const getNumberFormatOptions = () =>
|
||||
NUMBERFORMAT.map((item) => ({ label: item.label, value: item.suffix, color: item.color }));
|
||||
78
packages/sheets-numfmt/src/utils/pattern.ts
Normal file
78
packages/sheets-numfmt/src/utils/pattern.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* 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 { DateSystem, INumfmtLocaleTag } from '@univerjs/core';
|
||||
import type { FormatType } from '@univerjs/sheets';
|
||||
import { DEFAULT_NUMBER_FORMAT, numfmt } from '@univerjs/core';
|
||||
import { stripErrorMargin } from '@univerjs/engine-formula';
|
||||
|
||||
export const getPatternType = (pattern: string): FormatType => {
|
||||
const type = numfmt.getFormatInfo(pattern).type || 'unknown';
|
||||
if (type === 'general' && numfmt.tokenize(pattern).some((token) => token.type === numfmt.tokenTypes.CONDITION)) {
|
||||
return 'number';
|
||||
}
|
||||
|
||||
return type;
|
||||
};
|
||||
interface IPatternPreview {
|
||||
result: string;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
export const getPatternPreview = (
|
||||
pattern: string,
|
||||
value: number,
|
||||
locale: INumfmtLocaleTag = 'en',
|
||||
dateSystem?: DateSystem
|
||||
): IPatternPreview => {
|
||||
// in the source code of numfmt, the formatColor function will read the the partitions[3]
|
||||
try {
|
||||
const formatColor = numfmt.formatColor(pattern, value);
|
||||
const color = formatColor ? String(formatColor) : undefined;
|
||||
const result = numfmt.format(pattern, value, { locale, dateSystem, throws: false });
|
||||
if (value < 0) {
|
||||
// pay attention, controllers/ui.controller.ts
|
||||
// in the pattern, the negative value color may be upper case one , so if we read a color with UpperCase, we should return the color with lower case for our theme system
|
||||
return {
|
||||
result,
|
||||
color,
|
||||
};
|
||||
}
|
||||
return {
|
||||
result,
|
||||
};
|
||||
} catch (e) {
|
||||
console.warn('getPatternPreview error:', pattern, e);
|
||||
}
|
||||
|
||||
return {
|
||||
result: String(value),
|
||||
};
|
||||
};
|
||||
|
||||
export const getPatternPreviewIgnoreGeneral = (
|
||||
pattern: string,
|
||||
value: number,
|
||||
locale?: INumfmtLocaleTag,
|
||||
dateSystem?: DateSystem
|
||||
): IPatternPreview => {
|
||||
if (pattern === DEFAULT_NUMBER_FORMAT) {
|
||||
return {
|
||||
result: String(stripErrorMargin(value)), // In Excel, the default General format also needs to handle numeric precision.
|
||||
};
|
||||
}
|
||||
return getPatternPreview(pattern, value, locale, dateSystem);
|
||||
};
|
||||
40
packages/sheets-numfmt/src/utils/scientific-notation.ts
Normal file
40
packages/sheets-numfmt/src/utils/scientific-notation.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* 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 { ICellData, Nullable } from '@univerjs/core';
|
||||
import { CellValueType, isDefaultFormat, isTextFormat } from '@univerjs/core';
|
||||
import { getPatternType } from './pattern';
|
||||
|
||||
export const DEFAULT_SCIENTIFIC_NOTATION_FORMAT = '0.00E+00';
|
||||
|
||||
export function isScientificNotationNumericCell(cell: Nullable<ICellData>): boolean {
|
||||
return cell?.t === CellValueType.NUMBER && cell.v !== undefined && cell.v !== null && /e/i.test(String(cell.v));
|
||||
}
|
||||
|
||||
export function getScientificNotationFormatFromCell(cell: Nullable<ICellData>): string {
|
||||
const text = String(cell?.v);
|
||||
const eIndex = text.search(/e/i);
|
||||
const decimalIndex = text.indexOf('.');
|
||||
const decimalLength = eIndex > -1 && decimalIndex > -1 ? eIndex - decimalIndex - 1 : 0;
|
||||
|
||||
return `0.${'0'.repeat(Math.max(2, decimalLength))}E+00`;
|
||||
}
|
||||
|
||||
// Scientific notation numbers cannot be accurately expanded with JS number precision, so only formats that preserve
|
||||
// the notation or raw text are allowed.
|
||||
export function isAllowedPatternForScientificNotationNumber(pattern?: string): boolean {
|
||||
return isDefaultFormat(pattern) || isTextFormat(pattern) || getPatternType(pattern) === 'scientific';
|
||||
}
|
||||
Reference in New Issue
Block a user