fix(date-system): align Excel serial handling (#7594)

This commit is contained in:
wpxp123456
2026-08-25 19:45:01 +08:00
committed by GitHub
parent d9d5391c85
commit 8b2c5ad46d
213 changed files with 4402 additions and 1076 deletions
+7 -1
View File
@@ -32,18 +32,24 @@ export * from './lifecycle';
export * from './locale';
export { LRUHelper, LRUMap } from './lru/lru-map';
export * from './max-row-column';
export type { INumfmtLocaleTag } from './numfmt';
export type { IExcelDateTimeParts, INumfmtLocaleTag, ParseOptions } from './numfmt';
export {
currencySymbols,
DEFAULT_NUMBER_FORMAT,
DEFAULT_TEXT_FORMAT,
DEFAULT_TEXT_FORMAT_EXCEL,
EXCEL_1904_OFFSET,
excelDateTimePartsToSerial,
excelSerialToDateTimeParts,
getDateSystemMaxSerial,
getNumfmtLocaleTag,
getNumfmtParseValueFilter,
isDefaultFormat,
isPatternEqualWithoutDecimal,
isTextFormat,
numfmt,
} from './numfmt';
export { excelDateSerial, excelDateTimeSerial, excelSerialToDate, excelSerialToDateTime } from './numfmt/serial-date';
export * from './object-matrix';
export * from './random-id';
export { moveRangeByOffset, splitIntoGrid } from './range';
@@ -24,6 +24,7 @@ import * as numfmt from '../api';
it('preserves the numfmt 3.2.6 type exports', () => {
expectTypeOf<numfmt.FormatDateInfo['clockType']>().toEqualTypeOf<12 | 24>();
expectTypeOf<numfmt.FormatDateInfo['isDuration']>().toEqualTypeOf<boolean>();
expectTypeOf<numfmt.FormatInfo['type']>().toMatchTypeOf<string>();
expectTypeOf<numfmt.FormatToken['raw']>().toEqualTypeOf<string>();
expectTypeOf<numfmt.LocaleData['decimal']>().toEqualTypeOf<string>();
@@ -39,6 +40,7 @@ it('preserves the numfmt 3.2.6 runtime exports', () => {
'dec2frac',
'format',
'formatColor',
'getDateTimeEditPattern',
'getFormatDateInfo',
'getFormatInfo',
'getLocale',
@@ -19,7 +19,8 @@
// @vitest-environment node
import { describe, expect, it } from 'vitest';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { DateSystem } from '../../../types/enum/date-system';
import {
isValidDate,
parseBool,
@@ -29,6 +30,11 @@ import {
parseValue,
} from '../parse-value';
import { dateFromSerial, dateToSerial } from '../serial-date';
import { getNumfmtParseValueFilter } from '../utils';
afterEach(() => {
vi.useRealTimers();
});
describe('numfmt dates and value parsing', () => {
it('converts the spreadsheet serial epoch', () => {
@@ -41,6 +47,27 @@ describe('numfmt dates and value parsing', () => {
expect(dateFromSerial(60, { leap1900: false })).toEqual([1900, 2, 28, 0, 0, 0]);
});
it('parses dates into the selected Excel date system', () => {
expect(parseDate('1904-1-1', { dateSystem: DateSystem.Date1904 })).toEqual({ v: 0, z: 'yyyy-m-d' });
expect(parseDate('1/0/1900', { dateSystem: DateSystem.Date1900 })).toEqual({ v: 0, z: 'mm/dd/yyyy' });
expect(parseDate('2/29/1900', { dateSystem: DateSystem.Date1900 })).toEqual({ v: 60, z: 'm/d/yyyy' });
expect(parseDate('2/29/1900', { dateSystem: DateSystem.Date1904 })).toBeNull();
});
it('resolves an omitted year when parsing instead of when loading the module', () => {
vi.useFakeTimers();
vi.setSystemTime(new Date('2031-06-01T00:00:00Z'));
expect(parseDate('1/2', { dateSystem: DateSystem.Date1900 })).toEqual({ v: 47850, z: 'm/d' });
});
it('passes workbook parsing options through the common value filter', () => {
expect(getNumfmtParseValueFilter('31/12/2025', {
locale: 'en_GB',
dateSystem: DateSystem.Date1904,
})).toEqual({ v: 44560, z: 'dd/mm/yyyy' });
});
it('parses representative values', () => {
expect(parseNumber('1,234.50')).toEqual({ v: 1234.5, z: '#,##0.00' });
expect(parseDate('3/13/1989')).toEqual({ v: 32580, z: 'm/d/yyyy' });
@@ -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 { describe, expect, it } from 'vitest';
import { DateSystem } from '../../../types/enum/date-system';
import { excelDateSerial, excelDateTimePartsToSerial, excelDateTimeSerial, excelSerialToDate, excelSerialToDateTime, excelSerialToDateTimeParts } from '../serial-date';
describe('Excel date systems', () => {
it('preserves the Excel 1900 compatibility dates', () => {
expect(excelSerialToDateTimeParts(0, { dateSystem: DateSystem.Date1900 })).toEqual({
year: 1900,
month: 1,
day: 0,
hours: 0,
minutes: 0,
seconds: 0,
fractionalSecond: 0,
});
expect(excelSerialToDateTimeParts(60, { dateSystem: DateSystem.Date1900 })).toEqual({
year: 1900,
month: 2,
day: 29,
hours: 0,
minutes: 0,
seconds: 0,
fractionalSecond: 0,
});
});
it('uses 1904-01-01 as serial zero in the Excel 1904 system', () => {
const parts = {
year: 1904,
month: 1,
day: 1,
hours: 0,
minutes: 0,
seconds: 0,
fractionalSecond: 0,
};
expect(excelSerialToDateTimeParts(0, { dateSystem: DateSystem.Date1904 })).toEqual(parts);
expect(excelDateTimePartsToSerial(parts, { dateSystem: DateSystem.Date1904 })).toBe(0);
});
it('preserves fractional seconds without using a native Date', () => {
const parts = excelSerialToDateTimeParts(0.5242683912037037, { dateSystem: DateSystem.Date1900 });
expect(parts).toMatchObject({
year: 1900,
month: 1,
day: 0,
hours: 12,
minutes: 34,
seconds: 56,
});
expect(parts?.fractionalSecond).toBeCloseTo(0.789, 9);
});
it('rejects invalid calendar inputs', () => {
expect(excelSerialToDateTimeParts(-1, { dateSystem: DateSystem.Date1900 })).toBeNull();
expect(excelSerialToDateTimeParts(Number.POSITIVE_INFINITY, { dateSystem: DateSystem.Date1900 })).toBeNull();
expect(excelDateTimePartsToSerial({
year: 1900,
month: 2,
day: 29,
hours: 0,
minutes: 0,
seconds: 0,
fractionalSecond: 0,
}, { dateSystem: DateSystem.Date1904 })).toBeNull();
});
it('converts serials to UTC native Dates and normalizes pseudo-dates', () => {
expect(excelSerialToDate(0, DateSystem.Date1900).getTime()).toBe(Date.UTC(1899, 11, 31));
expect(excelSerialToDate(60, DateSystem.Date1900).getTime()).toBe(Date.UTC(1900, 1, 28));
expect(excelSerialToDate(0, DateSystem.Date1904).getTime()).toBe(Date.UTC(1904, 0, 1));
expect(excelSerialToDateTime(0.5, DateSystem.Date1904).getTime()).toBe(Date.UTC(1904, 0, 1, 12));
expect(Number.isNaN(excelSerialToDate(-1, DateSystem.Date1900).getTime())).toBe(true);
});
it('converts UTC native Dates to serials for both Excel date systems', () => {
expect(excelDateSerial(new Date(Date.UTC(1900, 1, 28)))).toBe(59);
expect(excelDateSerial(new Date(Date.UTC(1900, 2, 1)))).toBe(61);
expect(excelDateSerial(new Date(Date.UTC(1904, 0, 1)), DateSystem.Date1904)).toBe(0);
expect(excelDateTimeSerial(new Date(Date.UTC(2024, 0, 2, 12)))).toBe(45293.5);
expect(Number.isNaN(excelDateSerial(new Date(Number.NaN)))).toBe(true);
});
});
@@ -0,0 +1,62 @@
/**
* 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.
*/
// @vitest-environment node
import { describe, expect, it } from 'vitest';
import { getDateTimeEditPattern } from '../api';
const DATE_SERIAL = 45839;
describe('date-time edit pattern', () => {
it('completes normal numeric date formats while preserving their style', () => {
expect(getDateTimeEditPattern('mm-dd', DATE_SERIAL)).toBe('yyyy-mm-dd');
expect(getDateTimeEditPattern('m-d', DATE_SERIAL)).toBe('yyyy-m-d');
expect(getDateTimeEditPattern('dd/mm', DATE_SERIAL)).toBe('dd/mm/yyyy');
expect(getDateTimeEditPattern('d/m', DATE_SERIAL)).toBe('d/m/yyyy');
expect(getDateTimeEditPattern('yyyy-mm', DATE_SERIAL)).toBe('yyyy-mm-dd');
expect(getDateTimeEditPattern('yyyy/m', DATE_SERIAL)).toBe('yyyy/m/d');
expect(getDateTimeEditPattern('yyyy-m-d', DATE_SERIAL)).toBe('yyyy-m-d');
expect(getDateTimeEditPattern('dd/mm/yyyy', DATE_SERIAL)).toBe('dd/mm/yyyy');
});
it('completes date and time components together', () => {
expect(getDateTimeEditPattern('mm-dd h:mm', DATE_SERIAL)).toBe('yyyy-mm-dd h:mm:ss');
expect(getDateTimeEditPattern('dd/mm hh:mm', DATE_SERIAL)).toBe('dd/mm/yyyy hh:mm:ss');
expect(getDateTimeEditPattern('yyyy-m-d h:mm', DATE_SERIAL)).toBe('yyyy-m-d h:mm:ss');
expect(getDateTimeEditPattern('h:mm', DATE_SERIAL)).toBe('h:mm:ss');
expect(getDateTimeEditPattern('hh:mm AM/PM', DATE_SERIAL)).toBe('hh:mm:ss AM/PM');
expect(getDateTimeEditPattern('AM/PM h:mm', DATE_SERIAL)).toBe('AM/PM h:mm:ss');
});
it('falls back for complex dates and preserves a normal clock time', () => {
expect(getDateTimeEditPattern('mmm d', DATE_SERIAL)).toBe('yyyy/mm/dd');
expect(getDateTimeEditPattern('m"月"d"日"', DATE_SERIAL)).toBe('yyyy/mm/dd');
expect(getDateTimeEditPattern('dddd, mmmm d', DATE_SERIAL)).toBe('yyyy/mm/dd');
expect(getDateTimeEditPattern('m"月"d"日" h:mm', DATE_SERIAL)).toBe('yyyy/mm/dd h:mm:ss');
expect(getDateTimeEditPattern('mm/dd-yyyy', DATE_SERIAL)).toBe('yyyy/mm/dd');
});
it('uses the effective number-format section', () => {
expect(getDateTimeEditPattern('[<50000]mm-dd;dd/mm', DATE_SERIAL)).toBe('yyyy-mm-dd');
expect(getDateTimeEditPattern('[<40000]mm-dd;dd/mm', DATE_SERIAL)).toBe('dd/mm/yyyy');
});
it('hides subsecond precision and leaves durations to the duration editor', () => {
expect(getDateTimeEditPattern('h:mm', 0.5242683912037037)).toBe('h:mm:ss');
expect(getDateTimeEditPattern('[h]:mm', 1.25)).toBeNull();
});
});
@@ -31,6 +31,7 @@ describe('local numfmt API', () => {
'dec2frac',
'format',
'formatColor',
'getDateTimeEditPattern',
'getFormatDateInfo',
'getFormatInfo',
'getLocale',
@@ -416,6 +416,10 @@ numfmtTest('dateInfo', (t) => {
t.assertDateInfo('h', { hours: true });
t.assertDateInfo('h:mm', { hours: true, minutes: true });
t.assertDateInfo('h:m', { hours: true, minutes: true });
t.assertDateInfo('[h]:mm', { hours: true, minutes: true, isDuration: true });
t.assertDateInfo('[m]:ss', { minutes: true, seconds: true, isDuration: true });
t.assertDateInfo('[s]', { seconds: true, isDuration: true });
t.assertDateInfo('h:mm', { hours: true, minutes: true, isDuration: false });
t.assertDateInfo('ss', { seconds: true });
t.assertDateInfo('s', { seconds: true });
t.assertDateInfo('hh am/pm', { hours: true, clockType: 12 });
@@ -19,6 +19,7 @@
// @vitest-environment node
import { DateSystem } from '../../../types/enum/date-system';
import { format } from '../api';
import { getTimeZoneName, numfmtTest } from './test-utils';
@@ -26,6 +27,22 @@ import { getTimeZoneName, numfmtTest } from './test-utils';
const excelOpts = { dateSpanLarge: false, dateErrorNumber: false };
numfmtTest('option: Excel date system', (t) => {
t.format('yyyy-mm-dd', 0, '1904-01-01', { dateSystem: DateSystem.Date1904 });
t.format('yyyy-mm-dd', 1, '1904-01-02', { dateSystem: DateSystem.Date1904 });
t.format('dddd', 0, 'Friday', { dateSystem: DateSystem.Date1904 });
t.format('yyyy-mm-dd', 0, '1900-01-00', { dateSystem: DateSystem.Date1900 });
t.format('yyyy-mm-dd', 1, '1900-01-01', { dateSystem: DateSystem.Date1900 });
t.format('yyyy-mm-dd', 2958465, '9999-12-31', { dateSystem: DateSystem.Date1900 });
t.format('yyyy-mm-dd', 2958466, '######', { dateSystem: DateSystem.Date1900 });
t.format('yyyy-mm-dd', 2957003, '9999-12-31', { dateSystem: DateSystem.Date1904 });
t.format('yyyy-mm-dd', 2957004, '######', { dateSystem: DateSystem.Date1904 });
});
numfmtTest('default: Excel date overflow', (t) => {
t.format('yyyy', -1, '######');
});
numfmtTest('option: overflow', (t) => {
t.format('yyyy', -1, '######', { ...excelOpts });
t.format('yyyy', -1, '🦆', { ...excelOpts, overflow: '🦆' });
@@ -58,14 +75,14 @@ numfmtTest('option: leap1900', (t) => {
});
numfmtTest('option: dateErrorThrows', (t) => {
t.format('yyyy', -694325, '-694325', {});
t.format('yyyy', -1, '-1', { dateSpanLarge: false });
t.format('yyyy', -694325, '######', {});
t.format('yyyy', -1, '######', { dateSpanLarge: false });
t.formatThrows('yyyy', -1, '-1', { dateSpanLarge: false, dateErrorThrows: true });
t.formatThrows('yyyy', -694325, '-694325', { dateErrorThrows: true });
});
numfmtTest('option: dateErrorNumber', (t) => {
t.format('yyyy', -1, '-1', { dateSpanLarge: false });
t.format('yyyy', -1, '######', { dateSpanLarge: false });
t.format('yyyy', -1, '-1', { dateSpanLarge: false, dateErrorNumber: true });
t.format('yyyy', -1, '######', { dateSpanLarge: false, dateErrorNumber: false });
});
@@ -68,6 +68,7 @@ describe('numfmt pattern parser', () => {
hours: true,
minutes: true,
seconds: true,
isDuration: false,
clockType: 24,
});
});
@@ -78,6 +78,7 @@ const COMMON_DATE_INFO: FormatDateInfo = {
hours: false,
minutes: false,
seconds: false,
isDuration: false,
clockType: 24,
};
+8 -1
View File
@@ -61,6 +61,7 @@ import {
TOKEN_TEXT,
TOKEN_ZERO,
} from './constants';
import { createDateTimeEditPattern } from './date-time-edit-pattern';
import { dateInfo, info, isDate, isPercent, isText } from './format-info';
import { formatValue, formatColor as formatValueColor } from './format-number';
import { defaultOptions } from './options';
@@ -70,10 +71,10 @@ import { dateToSerial as handleDates } from './serial-date';
export { dec2frac } from './dec-to-frac';
export { addLocale, getLocale, parseLocale } from './locale';
export { parseBool, parseDate, parseNumber, parseTime, parseValue } from './parse-value';
export type { ParseOptions } from './parse-value';
export { round } from './round';
export { dateFromSerial, dateToSerial } from './serial-date';
export { tokenize } from './tokenize';
export type {
FormatDateInfo,
FormatInfo,
@@ -192,6 +193,12 @@ export function getFormatDateInfo(pattern: string): FormatDateInfo {
return data.dateInfo;
}
/** Completes omitted components in a calendar-date or clock pattern for editing; elapsed durations return null. */
export function getDateTimeEditPattern(pattern: string, value: number): string | null {
const data = prepareFormatterData(pattern, false);
return createDateTimeEditPattern(data.partitions, value);
}
/** A dictionary of the types used to identify token variants. */
export const tokenTypes = Object.freeze({
AMPM: TOKEN_AMPM,
@@ -0,0 +1,161 @@
/**
* 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 { FormatSection, ParsedFormatSection, RenderToken } from './types';
import { EPOCH_1900 } from './constants';
import { getValueFormatSection } from './format-number';
type DateComponentType = 'day' | 'month' | 'year';
interface IDateComponent {
type: DateComponentType;
pattern: string;
tokenIndex: number;
}
function isParsedSection(section: FormatSection | undefined): section is ParsedFormatSection {
return !!section && 'scale' in section;
}
function getDateComponent(token: RenderToken, tokenIndex: number): IDateComponent | null {
if (token.type === 'year' || token.type === 'year-short') {
return { type: 'year', pattern: 'yyyy', tokenIndex };
}
if (token.type === 'month') {
return { type: 'month', pattern: token.pad ? 'mm' : 'm', tokenIndex };
}
if (token.type === 'day') {
return { type: 'day', pattern: token.pad ? 'dd' : 'd', tokenIndex };
}
return null;
}
function isComplexDateToken(token: RenderToken): boolean {
return token.type.startsWith('b-year') || token.type.startsWith('monthname') || token.type.startsWith('weekday');
}
function getSeparator(tokens: RenderToken[], leftIndex: number, rightIndex: number): '-' | '/' | null {
if (rightIndex !== leftIndex + 2) {
return null;
}
const token = tokens[leftIndex + 1];
return token.value === '-' || token.value === '/' ? token.value : null;
}
function getMissingComponentPattern(components: IDateComponent[], type: 'day' | 'month'): string {
const related = components.find((component) => component.type === (type === 'day' ? 'month' : 'day'));
const padded = related?.pattern.length === 2;
if (type === 'day') {
return padded ? 'dd' : 'd';
}
return padded ? 'mm' : 'm';
}
function completeNumericDatePattern(components: IDateComponent[], separator: '-' | '/'): string | null {
const types = components.map((component) => component.type).join('');
if (new Set(components.map((component) => component.type)).size !== components.length) {
return null;
}
const patterns = new Map(components.map((component) => [component.type, component.pattern]));
patterns.set('year', 'yyyy');
if (!patterns.has('month')) {
patterns.set('month', getMissingComponentPattern(components, 'month'));
}
if (!patterns.has('day')) {
patterns.set('day', getMissingComponentPattern(components, 'day'));
}
// Preserve an unambiguous existing order; when the year is omitted, put it where Excel exposes it while editing.
const completeOrder: Record<string, DateComponentType[]> = {
daymonth: ['day', 'month', 'year'],
daymonthyear: ['day', 'month', 'year'],
dayyear: ['day', 'month', 'year'],
monthday: ['year', 'month', 'day'],
monthdayyear: ['month', 'day', 'year'],
monthyear: ['month', 'day', 'year'],
yearmonth: ['year', 'month', 'day'],
yearmonthday: ['year', 'month', 'day'],
yearday: ['year', 'month', 'day'],
};
const order = completeOrder[types];
return order ? order.map((type) => patterns.get(type)).join(separator) : null;
}
function getDatePattern(section: ParsedFormatSection): string | null {
const components = section.tokens
.map((token, tokenIndex) => getDateComponent(token, tokenIndex))
.filter((component): component is IDateComponent => component !== null);
if (!components.length) {
return null;
}
// Only numeric dates with one consistent '-' or '/' separator are safe to extend without changing their meaning.
if (
components.length < 2 ||
components.length > 3 ||
section.locale ||
section.date_system !== EPOCH_1900 ||
section.tokens.some(isComplexDateToken)
) {
return 'yyyy/mm/dd';
}
const separators = components.slice(1).map((component, index) => (
getSeparator(section.tokens, components[index].tokenIndex, component.tokenIndex)
));
const separator = separators[0];
if (!separator || separators.some((item) => item !== separator)) {
return 'yyyy/mm/dd';
}
return completeNumericDatePattern(components, separator) ?? 'yyyy/mm/dd';
}
function getTimePattern(section: ParsedFormatSection): string | null {
const hourIndex = section.tokens.findIndex((token) => token.type === 'hour');
const minuteToken = section.tokens.find((token) => token.type === 'min');
const ampmIndex = section.tokens.findIndex((token) => token.type === 'ampm');
const ampmToken = ampmIndex < 0 ? undefined : section.tokens[ampmIndex];
const hasTime = hourIndex >= 0 || !!minuteToken || section.tokens.some((token) => token.type === 'sec') || !!ampmToken;
if (!hasTime) {
return null;
}
const hourToken = hourIndex < 0 ? undefined : section.tokens[hourIndex];
const hour = hourToken?.pad ? 'hh' : 'h';
const minute = minuteToken?.pad === false ? 'm' : 'mm';
const clock = `${hour}:${minute}:ss`;
if (!ampmToken || typeof ampmToken.value !== 'string') {
return clock;
}
return ampmIndex < hourIndex ? `${ampmToken.value} ${clock}` : `${clock} ${ampmToken.value}`;
}
export function createDateTimeEditPattern(
partitions: Array<FormatSection | undefined>,
value: number
): string | null {
const section = getValueFormatSection(value, partitions);
if (!isParsedSection(section) || section.tokens.some((token) => token.type.endsWith('-elap'))) {
return null;
}
const datePattern = getDatePattern(section);
const timePattern = getTimePattern(section);
if (datePattern && timePattern) {
return `${datePattern} ${timePattern}`;
}
return datePattern ?? timePattern;
}
@@ -229,6 +229,7 @@ export function dateInfo(partitions: Array<FormatSection | undefined>): FormatDa
hours: !!(date & u_HOUR),
minutes: !!(date & u_MIN),
seconds: !!(date & u_SEC),
isDuration: positive?.tokens.some((token) => token.type.endsWith('-elap')) ?? false,
clockType: positive?.clock === 12 ? 12 : 24,
};
}
@@ -38,7 +38,8 @@ function isParsedSection(part: FormatSection | undefined): part is ParsedFormatS
return !!part && 'scale' in part;
}
function getPart(
/** Select the same conditional/sign format section for both display formatting and editor-value formatting. */
export function getValueFormatSection(
value: number | bigint,
parts: Array<FormatSection | undefined>
): FormatSection | undefined {
@@ -89,7 +90,7 @@ export function formatColor(
// eslint-disable-next-line unicorn/prefer-number-properties
isFinite(value as number)
) {
part = getPart(value, parts);
part = getValueFormatSection(value, parts);
}
if (isParsedSection(part) && part.color) {
color = part.color;
@@ -130,7 +131,7 @@ export function formatValue(
return ((renderValue as number) < 0 ? resolvedLocale.negative : '') + resolvedLocale.infinity;
}
// find and run the pattern part that applies to this number
const part = getPart(value as number | bigint, parts);
const part = getValueFormatSection(value as number | bigint, parts);
const decimalValue =
part && typeof value === 'number' && Number.isFinite(value)
? decimalFromNumber(value)
+4
View File
@@ -15,12 +15,16 @@
*/
export * as numfmt from './api';
export type { ParseOptions } from './parse-value';
export type { IExcelDateTimeParts } from './serial-date';
export { EXCEL_1904_OFFSET, excelDateTimePartsToSerial, excelSerialToDateTimeParts, getDateSystemMaxSerial } from './serial-date';
export type { INumfmtLocaleTag } from './utils';
export {
currencySymbols,
DEFAULT_NUMBER_FORMAT,
DEFAULT_TEXT_FORMAT,
DEFAULT_TEXT_FORMAT_EXCEL,
getNumfmtLocaleTag,
getNumfmtParseValueFilter,
isDefaultFormat,
isPatternEqualWithoutDecimal,
+4 -2
View File
@@ -18,6 +18,7 @@
// See packages/core/src/shared/numfmt/LICENSE.
import type { ResolvedFormatOptions } from './types';
import { DateSystem } from '../../types/enum/date-system';
export const defaultOptions: Readonly<ResolvedFormatOptions> = {
// Overflow error string
@@ -25,13 +26,14 @@ export const defaultOptions: Readonly<ResolvedFormatOptions> = {
// Should it throw when there is an overflow error?
dateErrorThrows: false,
// Should it emit a number when date has an overflow error? (Sheets does this)
dateErrorNumber: true, // dateErrorThrow needs to be off!
dateErrorNumber: false, // dateErrorThrow needs to be off!
// Should it emit a number when bigint has an is an overflow error?
bigintErrorNumber: false,
// Sheets mode (see #3)
dateSpanLarge: true,
dateSpanLarge: false,
// Simulate the Lotus 1-2-3 leap year bug
leap1900: true,
dateSystem: DateSystem.Date1900,
// Emit regular vs. non-breaking spaces
nbsp: false,
// Robust/throw mode
+35 -20
View File
@@ -18,12 +18,15 @@
// See packages/core/src/shared/numfmt/LICENSE.
import type { LocaleData, ParseData } from './types';
import { DateSystem } from '../../types/enum/date-system';
import { currencySymbols, reCurrencySymbols } from './constants';
import { defaultLocale, getLocale } from './locale';
import { excelDateTimePartsToSerial } from './serial-date';
// eslint-disable-next-line ts/naming-convention
export interface ParseOptions {
locale?: string;
dateSystem?: DateSystem;
}
/*
@@ -182,8 +185,6 @@ okDateFormats.forEach((format) => {
}
});
const currentYear = new Date().getUTCFullYear();
const PT = '.';
const CM = ',';
const SP = ' ';
@@ -396,9 +397,10 @@ interface IDateLocaleData {
day: LocaleLookup[];
dp: boolean;
locale?: string;
dateSystem: DateSystem;
}
type DateLocaleLookups = Omit<IDateLocaleData, 'locale'>;
type DateLocaleLookups = Omit<IDateLocaleData, 'dateSystem' | 'locale'>;
interface ICachedDateLocaleLookups {
lookups: DateLocaleLookups;
@@ -495,7 +497,7 @@ const nextToken = (
);
}
} else if (token === 'j' || token === 'd') {
const match = /^(0?[1-9]|1\d|2\d|3[01])\b/.exec(string);
const match = /^(0{1,2}|0?[1-9]|1\d|2\d|3[01])\b/.exec(string);
if (match) {
result = nextToken(
string.slice(match[0].length),
@@ -569,10 +571,11 @@ const nextToken = (
throw new Error(`Unknown date token "${token}"`);
}
}
if (
result &&
isValidDate(data.year || 1916, data.month || 1, data.day ? +data.day : 1)
) {
const year = result?.year || 1916;
const month = result?.month || 1;
const day = result?.day ? +result.day : 1;
const isDate1900SerialZero = localeData.dateSystem === DateSystem.Date1900 && year === 1900 && month === 1 && day === 0;
if (result && (isDate1900SerialZero || isValidDate(year, month, day))) {
return result;
}
}
@@ -627,12 +630,12 @@ const getDateLocaleLookups = (locale: LocaleData): DateLocaleLookups => {
};
/** Parse a date or datetime string and return its serial value and format. */
// eslint-disable-next-line complexity
export function parseDate(value: string, options: ParseOptions = {}): ParseData<number> | null {
const l10n = getLocale(options.locale || '') || defaultLocale;
const localeData: IDateLocaleData = {
...getDateLocaleLookups(l10n),
locale: options.locale,
dateSystem: options.dateSystem ?? DateSystem.Date1900,
};
const date = nextToken(
normDateStr(value),
@@ -650,20 +653,32 @@ export function parseDate(value: string, options: ParseOptions = {}): ParseData<
return null;
}
const year = +(date.year ?? currentYear);
const year = +(date.year ?? new Date().getUTCFullYear());
const day = date.day ?? '1';
let epoch = -Infinity;
if (year < 1900) {
const dateSystem = options.dateSystem ?? DateSystem.Date1900;
const dateSerial = excelDateTimePartsToSerial({
year,
month: date.month,
day: +day,
hours: 0,
minutes: 0,
seconds: 0,
fractionalSecond: 0,
}, { dateSystem });
if (dateSerial == null) {
return null;
} else if (year <= 1900 && date.month <= 2) {
epoch = 25568;
} else if (year < 10000) {
epoch = 25569;
}
const dateValue = (
Date.UTC(year, date.month - 1, +day) / 864e5
) + epoch + (date.time || 0);
if (dateValue < 0 || dateValue > 2958465) {
const dateValue = dateSerial + (date.time || 0);
const maxDateSerial = excelDateTimePartsToSerial({
year: 9999,
month: 12,
day: 31,
hours: 0,
minutes: 0,
seconds: 0,
fractionalSecond: 0,
}, { dateSystem })!;
if (dateValue >= maxDateSerial + 1) {
return null;
}
+13 -6
View File
@@ -25,11 +25,12 @@ import type {
ParsedFormatSection,
ResolvedFormatOptions,
} from './types';
import { DateSystem } from '../../types/enum/date-system';
import { clamp } from './clamp';
import {
EPOCH_1317,
EPOCH_1904,
MAX_L_DATE,
MAX_S_DATE,
MIN_L_DATE,
MIN_S_DATE,
u_CSEC,
@@ -42,17 +43,19 @@ import { general } from './general';
import { defaultLocale } from './locale';
import { getExponent, getSignificand } from './number-props';
import { pad } from './pad';
import { getDateSystemMaxSerial } from './serial-date';
import { toYMD } from './to-ymd';
const DAYSIZE = 86400;
type RuntimeFormatSection = ParsedFormatSection & Partial<ErrorFormatSection>;
function dateOverflows(inputValue: number, roundedValue: number, bigRange: boolean): boolean {
function dateOverflows(inputValue: number, roundedValue: number, bigRange: boolean, dateSystem: DateSystem): boolean {
if (bigRange) {
return inputValue < MIN_L_DATE || roundedValue >= MAX_L_DATE;
}
return inputValue < MIN_S_DATE || roundedValue >= MAX_S_DATE;
const maxDate = getDateSystemMaxSerial(dateSystem) + 1;
return inputValue < MIN_S_DATE || roundedValue >= maxDate;
}
function legacyRound(number: number, places = 0): number {
@@ -263,8 +266,12 @@ export function runPart(
}
}
// serial date/time to gregorian calendar
// Explicit non-Gregorian calendar tokens keep their own epoch; ordinary date tokens follow the workbook's system.
const dateSystem = section.date_system === EPOCH_1317
? EPOCH_1317
: options.dateSystem === DateSystem.Date1904 ? EPOCH_1904 : section.date_system;
if (date || section.date_system) {
const dateOutput = toYMD(numericValue, section.date_system, options.leap1900);
const dateOutput = toYMD(numericValue, dateSystem, options.leap1900);
year = dateOutput[0];
month = dateOutput[1];
day = dateOutput[2];
@@ -275,10 +282,10 @@ export function runPart(
minute = Math.floor(normalizedTime / 60) % 60;
hour = Math.floor((normalizedTime / 60) / 60) % 60;
}
weekday = (6 + date) % 7;
weekday = ((dateSystem === EPOCH_1904 ? 5 : 6) + date) % 7;
if (
section.date_eval &&
dateOverflows(numericValue, date + (time / DAYSIZE), options.dateSpanLarge)
dateOverflows(numericValue, date + (time / DAYSIZE), options.dateSpanLarge, options.dateSystem)
) {
// if value is out of bounds and formatting is date Excel emits a
// stream of "######" that fills the cell width.
@@ -18,11 +18,213 @@
// See packages/core/src/shared/numfmt/LICENSE.
import type { DateTimeParts } from './types';
import { DateSystem } from '../../types/enum/date-system';
import { toYMD } from './to-ymd';
const floor = Math.floor;
const DAYSIZE = 86400;
const MILLISECONDS_PER_DAY = DAYSIZE * 1000;
const dateUTC = Date.UTC as (...values: unknown[]) => number;
// The 1904 epoch is 1,461 real days after the 1900 epoch; Excel's fictitious 1900-02-29 adds one serial day.
export const EXCEL_1904_OFFSET = 1462;
// Excel's supported calendar range ends at 9999-12-31.
const EXCEL_1900_MAX_SERIAL = 2958465;
const MAX_YEAR = 9999;
/**
* Decoded Excel calendar and clock fields.
* In the 1900 system, serial 0 returns 1900-01-00 and serial 60 returns the fictitious 1900-02-29.
* Callers converting these fields to a native Date must choose how to normalize those two pseudo-dates.
*/
export interface IExcelDateTimeParts {
year: number;
month: number;
day: number;
hours: number;
minutes: number;
seconds: number;
fractionalSecond: number;
}
interface IDateSystemOptions {
dateSystem: DateSystem;
}
export function getDateSystemMaxSerial(dateSystem = DateSystem.Date1900): number {
return dateSystem === DateSystem.Date1904
? EXCEL_1900_MAX_SERIAL - EXCEL_1904_OFFSET
: EXCEL_1900_MAX_SERIAL;
}
function isLeapYear(year: number): boolean {
return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
}
function isValidGregorianDate(year: number, month: number, day: number): boolean {
if (!Number.isInteger(year) || !Number.isInteger(month) || !Number.isInteger(day) || year < 1 || year > MAX_YEAR || month < 1 || month > 12) {
return false;
}
const daysInMonth = [31, isLeapYear(year) ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
return day >= 1 && day <= daysInMonth[month - 1];
}
function civilDateToUnixDays(year: number, month: number, day: number): number {
// Use pure proleptic-Gregorian arithmetic so native Date normalization and host time zones cannot affect serials.
const adjustedYear = year - (month <= 2 ? 1 : 0);
const era = Math.floor(adjustedYear / 400);
const yearOfEra = adjustedYear - era * 400;
const adjustedMonth = month + (month > 2 ? -3 : 9);
const dayOfYear = Math.floor((153 * adjustedMonth + 2) / 5) + day - 1;
const dayOfEra = yearOfEra * 365 + Math.floor(yearOfEra / 4) - Math.floor(yearOfEra / 100) + dayOfYear;
return era * 146097 + dayOfEra - 719468;
}
function datePartsToExcel1900Serial(year: number, month: number, day: number): number | null {
// Excel exposes both serial 0 (1900-01-00) and serial 60 (the Lotus 1-2-3 leap-year bug) as editable dates.
if (year === 1900 && month === 1 && day === 0) {
return 0;
}
if (year === 1900 && month === 2 && day === 29) {
return 60;
}
if (!isValidGregorianDate(year, month, day)) {
return null;
}
const unixDays = civilDateToUnixDays(year, month, day);
// Serial 60 is reserved for the fictitious leap day, so real dates from 1900-03-01 use the larger offset.
return unixDays + (unixDays <= -25509 ? 25568 : 25569);
}
export function excelSerialToDateTimeParts(
serial: number,
options: IDateSystemOptions
): IExcelDateTimeParts | null {
if (!Number.isFinite(serial) || serial < 0) {
return null;
}
const wholeDays = Math.floor(serial);
const excel1900Serial = options.dateSystem === DateSystem.Date1904
? wholeDays + EXCEL_1904_OFFSET
: wholeDays;
const [year, month, day] = toYMD(excel1900Serial, 0, true);
if (year > MAX_YEAR) {
return null;
}
const totalSeconds = (serial - wholeDays) * DAYSIZE;
const wholeSeconds = Math.floor(totalSeconds);
return {
year,
month,
day,
hours: Math.floor(wholeSeconds / 3600),
minutes: Math.floor(wholeSeconds / 60) % 60,
seconds: wholeSeconds % 60,
fractionalSecond: totalSeconds - wholeSeconds,
};
}
function excelDateTimePartsToDate(parts: IExcelDateTimeParts, includeTime: boolean): Date {
// Native Date cannot represent Excel's serial 0 or fictitious 1900-02-29.
// Normalize those compatibility values to the nearest representable UTC date.
const day = parts.year === 1900 && parts.month === 2 && parts.day === 29 ? 28 : parts.day;
return new Date(Date.UTC(
parts.year,
parts.month - 1,
day,
includeTime ? parts.hours : 0,
includeTime ? parts.minutes : 0,
includeTime ? parts.seconds : 0,
includeTime ? Math.round(parts.fractionalSecond * 1000) : 0
));
}
/** Convert an Excel serial to a native UTC Date, keeping only its calendar date. Returns an invalid Date for invalid serials. */
export function excelSerialToDate(serial: number, dateSystem = DateSystem.Date1900): Date {
const parts = excelSerialToDateTimeParts(serial, { dateSystem });
return parts ? excelDateTimePartsToDate(parts, false) : new Date(Number.NaN);
}
/** Convert an Excel serial to a native UTC Date, including its time fraction. Returns an invalid Date for invalid serials. */
export function excelSerialToDateTime(serial: number, dateSystem = DateSystem.Date1900): Date {
const parts = excelSerialToDateTimeParts(serial, { dateSystem });
return parts ? excelDateTimePartsToDate(parts, true) : new Date(Number.NaN);
}
export function excelDateTimePartsToSerial(
parts: IExcelDateTimeParts,
options: IDateSystemOptions
): number | null {
const { year, month, day, hours, minutes, seconds, fractionalSecond } = parts;
if (
!Number.isInteger(hours) || hours < 0 || hours > 23 ||
!Number.isInteger(minutes) || minutes < 0 || minutes > 59 ||
!Number.isInteger(seconds) || seconds < 0 || seconds > 59 ||
!Number.isFinite(fractionalSecond) || fractionalSecond < 0 || fractionalSecond >= 1
) {
return null;
}
if (options.dateSystem === DateSystem.Date1900 && year < 1900) {
return null;
}
let dateSerial = datePartsToExcel1900Serial(year, month, day);
if (dateSerial == null) {
return null;
}
if (options.dateSystem === DateSystem.Date1904) {
if ((year === 1900 && month === 1 && day === 0) || (year === 1900 && month === 2 && day === 29)) {
return null;
}
dateSerial -= EXCEL_1904_OFFSET;
}
if (dateSerial < 0) {
return null;
}
return dateSerial + (hours * 3600 + minutes * 60 + seconds + fractionalSecond) / DAYSIZE;
}
function dateToExcelSerial(date: Date, includeTime: boolean, dateSystem: DateSystem): number {
if (!Number.isFinite(date.getTime())) {
return Number.NaN;
}
const serial = excelDateTimePartsToSerial({
year: date.getUTCFullYear(),
month: date.getUTCMonth() + 1,
day: date.getUTCDate(),
hours: includeTime ? date.getUTCHours() : 0,
minutes: includeTime ? date.getUTCMinutes() : 0,
seconds: includeTime ? date.getUTCSeconds() : 0,
fractionalSecond: includeTime ? date.getUTCMilliseconds() / 1000 : 0,
}, { dateSystem });
if (serial != null) {
return serial;
}
// Keep compatibility for native dates outside the selected Excel epoch.
const dateTime = includeTime
? date.getTime()
: Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate());
const epoch = dateSystem === DateSystem.Date1904 ? Date.UTC(1904, 0, 1) : Date.UTC(1899, 11, 31);
return (dateTime - epoch) / MILLISECONDS_PER_DAY;
}
/** Convert a native UTC Date's calendar fields to an Excel date serial. */
export function excelDateSerial(date: Date, dateSystem = DateSystem.Date1900): number {
return dateToExcelSerial(date, false, dateSystem);
}
/** Convert a native UTC Date's calendar and clock fields to an Excel serial. */
export function excelDateTimeSerial(date: Date, dateSystem = DateSystem.Date1900): number {
return dateToExcelSerial(date, true, dateSystem);
}
/**
* Convert a native JavaScript Date, or array to a spreadsheet serial date.
+5
View File
@@ -17,6 +17,8 @@
// Derived from numfmt 3.2.6 (MIT), commit c2cfdfa01bb1f24df51e985825671eb480daed4c.
// See packages/core/src/shared/numfmt/LICENSE.
import type { DateSystem } from '../../types/enum/date-system';
export type ConditionOperator = '<' | '<=' | '=' | '>=' | '>' | '<>';
export type FormatCondition = [ConditionOperator, number];
@@ -70,6 +72,7 @@ export interface FormatOptions {
bigintErrorNumber?: boolean;
dateSpanLarge?: boolean;
leap1900?: boolean;
dateSystem?: DateSystem;
nbsp?: boolean;
throws?: boolean;
invalid?: string;
@@ -88,6 +91,7 @@ export interface ResolvedFormatOptions {
bigintErrorNumber: boolean;
dateSpanLarge: boolean;
leap1900: boolean;
dateSystem: DateSystem;
nbsp: boolean;
throws: boolean;
invalid: string;
@@ -190,6 +194,7 @@ export interface FormatDateInfo {
hours: boolean;
minutes: boolean;
seconds: boolean;
isDuration: boolean;
clockType: 12 | 24;
}
+46 -2
View File
@@ -15,7 +15,9 @@
*/
import type { ParseData } from './api';
import type { ParseOptions } from './parse-value';
import { regexp } from '../../common/regexp';
import { LocaleType } from '../../types/enum/locale-type';
import { parseDate, parseNumber, parseTime, tokenize, tokenTypes } from './api';
export const DEFAULT_TEXT_FORMAT = '@@@'; // Compatible with old data
@@ -60,6 +62,48 @@ export type INumfmtLocaleTag =
| 'tr'
| 'vi';
export function getNumfmtLocaleTag(locale: LocaleType): INumfmtLocaleTag {
switch (locale) {
case LocaleType.FR_FR:
return 'fr';
case LocaleType.RU_RU:
return 'ru';
case LocaleType.VI_VN:
return 'vi';
case LocaleType.ZH_CN:
return 'zh-CN';
case LocaleType.KO_KR:
return 'ko';
case LocaleType.ZH_TW:
return 'zh-TW';
case LocaleType.ZH_HK:
return 'zh-HK';
case LocaleType.ES_ES:
case LocaleType.CA_ES:
return 'es';
case LocaleType.SK_SK:
return 'sk';
case LocaleType.JA_JP:
return 'ja';
case LocaleType.PT_BR:
return 'pt';
case LocaleType.DE_DE:
return 'de';
case LocaleType.IT_IT:
return 'it';
case LocaleType.ID_ID:
return 'id';
case LocaleType.PL_PL:
return 'pl';
case LocaleType.AR_SA:
return 'ar';
case LocaleType.EN_US:
case LocaleType.FA_IR:
default:
return 'en';
}
}
/**
* Determines whether two patterns are equal, excluding differences in decimal places.
* This function ignores the decimal part of the patterns and the positive color will be ignored but negative color will be considered.
@@ -165,8 +209,8 @@ const CURRENCY_SYMBOL_PREFIX_REG = new RegExp(`^${regexp.charset(...Array.from(n
/**
* Get the numfmt parse value, and filter out the parse error.
*/
export const getNumfmtParseValueFilter = (value: string): ParseData | null => {
const parseData = parseDate(value) ?? parseTime(value) ?? parseNumber(value);
export const getNumfmtParseValueFilter = (value: string, options: ParseOptions = {}): ParseData | null => {
const parseData = parseDate(value, options) ?? parseTime(value, options) ?? parseNumber(value, options);
if (!parseData) return null;
@@ -18,7 +18,7 @@ import type { Univer } from '../../univer';
import type { IWorkbookData } from '../typedef';
import type { Workbook } from '../workbook';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { BooleanNumber } from '../../types/enum';
import { BooleanNumber, DateSystem } from '../../types/enum';
import { LocaleType } from '../../types/enum/locale-type';
import { createCoreTestBed } from './create-core-test-bed';
@@ -79,6 +79,21 @@ describe('Test workbook', () => {
expect(workbook.getConfig()).toBe(workbook.getSnapshot());
});
it('should resolve the workbook date system without materializing legacy metadata', () => {
expect(workbook.getDateSystem()).toBe(DateSystem.Date1900);
expect(workbook.save().dateSystem).toBeUndefined();
const date1904TestBed = createCoreTestBed({
...workbook.save(),
id: 'date-1904',
dateSystem: DateSystem.Date1904,
});
expect(date1904TestBed.sheet.getDateSystem()).toBe(DateSystem.Date1904);
expect(date1904TestBed.sheet.save().dateSystem).toBe(DateSystem.Date1904);
date1904TestBed.univer.dispose();
});
it('should manage sheets and lookups', () => {
expect(workbook.addWorksheet('sheet1', 1, { id: 'sheet1', name: 'Dup' })).toBe(false);
expect(workbook.addWorksheet('sheet2', 1, { id: 'sheet2', name: 'Sheet-002' })).toBe(true);
@@ -16,6 +16,7 @@
import type { IWorkbookData } from './typedef';
import pkg from '../../package.json';
import { DateSystem } from '../types/enum/date-system';
import { LocaleType } from '../types/enum/locale-type';
export function getEmptySnapshot(
@@ -29,6 +30,7 @@ export function getEmptySnapshot(
name,
appVersion: pkg.version,
locale,
dateSystem: DateSystem.Date1900,
styles: {},
sheets: {},
resources: [],
+4
View File
@@ -17,6 +17,7 @@
import type { IResources } from '../services/resource-manager/type';
import type { IObjectArrayPrimitiveType, IObjectMatrixPrimitiveType, Nullable } from '../shared';
import type { BooleanNumber } from '../types/enum';
import type { DateSystem } from '../types/enum/date-system';
import type { LocaleType } from '../types/enum/locale-type';
import type { IDocumentData } from '../types/interfaces';
import type { ICellCustomRender } from '../types/interfaces/i-cell-custom-render';
@@ -53,6 +54,9 @@ export interface IWorkbookData {
*/
locale: LocaleType;
/** Date serial system used by this workbook. Missing values mean Excel 1900. */
dateSystem?: DateSystem;
/**
* Style references.
*/
+13
View File
@@ -24,6 +24,7 @@ import { ILogService } from '../services/log/log.service';
import { Tools } from '../shared';
import { generateRandomId } from '../shared/random-id';
import { BooleanNumber } from '../types/enum';
import { DateSystem } from '../types/enum/date-system';
import { getEmptySnapshot } from './empty-snapshot';
import { Styles } from './styles';
import { Worksheet } from './worksheet';
@@ -93,6 +94,12 @@ export class Workbook extends UnitModel<IWorkbookData, UniverInstanceType.UNIVER
this._snapshot = DEFAULT_WORKBOOK;
} else {
this._snapshot = Tools.commonExtend(DEFAULT_WORKBOOK, workbookData);
if (workbookData.dateSystem == null) {
delete this._snapshot.dateSystem;
} else if (workbookData.dateSystem !== DateSystem.Date1900 && workbookData.dateSystem !== DateSystem.Date1904) {
this._logService.warn('[Workbook]', `Unknown date system "${String(workbookData.dateSystem)}"; falling back to Excel 1900.`);
this._snapshot.dateSystem = DateSystem.Date1900;
}
}
const { styles } = this._snapshot;
@@ -153,6 +160,12 @@ export class Workbook extends UnitModel<IWorkbookData, UniverInstanceType.UNIVER
return this._unitId;
}
getDateSystem(): DateSystem {
return this._snapshot.dateSystem === DateSystem.Date1904
? DateSystem.Date1904
: DateSystem.Date1900;
}
override getRev(): number {
return this._snapshot.rev ?? 1; // the revision number should start with one
}
@@ -0,0 +1,23 @@
/**
* 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.
*/
/** Workbook date serial systems supported by Univer. */
export enum DateSystem {
/** Serial 1 is 1900-01-01; serials 0 and 60 preserve Excel's compatibility-only dates. */
Date1900 = 'date1900',
/** Serial 0 is 1904-01-01. */
Date1904 = 'date1904',
}
+1
View File
@@ -19,6 +19,7 @@ export * from './border-style-types';
export * from './color-type';
export * from './common-hide-types';
export * from './copy-paste-type';
export * from './date-system';
export * from './delete-direction';
export * from './developer-metadata-visibility';
export * from './dimension';
@@ -56,14 +56,15 @@ function DayButton(props: ButtonHTMLAttributes<HTMLButtonElement>) {
interface ICalendarProps {
className?: string;
showTime?: boolean;
showSelection?: boolean;
max?: Date;
min?: Date;
value?: Date;
onValueChange?: (date: Date) => void;
onValueChange?: (date: Date, changeType: 'date' | 'time') => void;
}
export function Calendar(props: ICalendarProps) {
const { className, max, min, showTime = false, value, onValueChange } = props;
const { className, max, min, showSelection = true, showTime = false, value, onValueChange } = props;
const { direction = 'ltr', locale } = useContext(ConfigContext);
@@ -151,7 +152,7 @@ export function Calendar(props: ICalendarProps) {
}, [daysInMonth, firstDay]);
function isSelected(day: number) {
return day && currentYear === value?.getFullYear() && currentMonth === value?.getMonth() && day === value?.getDate();
return showSelection && day && currentYear === value?.getFullYear() && currentMonth === value?.getMonth() && day === value?.getDate();
}
function isToday(day: number) {
@@ -163,7 +164,8 @@ export function Calendar(props: ICalendarProps) {
const hours = value?.getHours() ?? today.getHours();
const minutes = value?.getMinutes() ?? today.getMinutes();
const seconds = value?.getSeconds() ?? today.getSeconds();
const d = new Date(currentYear, currentMonth, day, hours, minutes, seconds);
const milliseconds = value?.getMilliseconds() ?? today.getMilliseconds();
const d = new Date(currentYear, currentMonth, day, hours, minutes, seconds, milliseconds);
if (min && d < min) return true;
if (max && d > max) return true;
return false;
@@ -174,20 +176,22 @@ export function Calendar(props: ICalendarProps) {
const hours = value?.getHours() ?? today.getHours();
const minutes = value?.getMinutes() ?? today.getMinutes();
const seconds = value?.getSeconds() ?? today.getSeconds();
const milliseconds = value?.getMilliseconds() ?? today.getMilliseconds();
const selectedDate = new Date(currentYear, currentMonth, day, hours, minutes, seconds);
const selectedDate = new Date(currentYear, currentMonth, day, hours, minutes, seconds, milliseconds);
onValueChange?.(selectedDate);
onValueChange?.(selectedDate, 'date');
}
function handleChangeTime(time: Date) {
const hours = time.getHours();
const minutes = time.getMinutes();
const seconds = time.getSeconds();
const milliseconds = time.getMilliseconds();
const updatedDate = new Date(currentYear, currentMonth, value?.getDate() ?? today.getDate(), hours, minutes, seconds);
const updatedDate = new Date(currentYear, currentMonth, value?.getDate() ?? today.getDate(), hours, minutes, seconds, milliseconds);
onValueChange?.(updatedDate);
onValueChange?.(updatedDate, 'time');
}
return (
@@ -58,6 +58,20 @@ describe('Calendar', () => {
expect(calledDate.getDate()).toBe(20);
});
it('should preserve fractional seconds when only the calendar date changes', () => {
const date = new Date(2023, 7, 15, 1, 2, 3, 456);
const { getByText } = render(
<ConfigProvider mountContainer={null} locale={{ Calendar: enUS.design.Calendar }}>
<Calendar value={date} onValueChange={onChange} />
</ConfigProvider>
);
getByText('20').click();
expect((onChange.mock.calls[0][0] as Date).getMilliseconds()).toBe(456);
expect(onChange).toHaveBeenCalledWith(expect.any(Date), 'date');
});
it('should show TimeInput when showTime is true', () => {
const { container } = renderCalendar({ showTime: true });
expect(container.querySelector('[data-u-comp="time-input"]')).toBeInTheDocument();
@@ -138,6 +152,6 @@ describe('Calendar', () => {
const timeInput = container.querySelector('input[type="time"]') as HTMLInputElement;
fireEvent.change(timeInput, { target: { value: '11:22:33' } });
expect(onChange).toHaveBeenCalled();
expect(onChange).toHaveBeenCalledWith(expect.any(Date), 'time');
});
});
@@ -48,4 +48,19 @@ describe('TimeInput', () => {
expect(changed.getMinutes()).toBe(34);
expect(changed.getSeconds()).toBe(56);
});
it('should hide stored fractional seconds and clear them when time changes', () => {
const onValueChange = vi.fn();
const value = new Date(2024, 0, 1, 1, 2, 3, 456);
const { container, getByDisplayValue } = render(<TimeInput value={value} onValueChange={onValueChange} />);
expect(getByDisplayValue('01:02:03')).toBeInTheDocument();
const input = container.querySelector('input[type="time"]') as HTMLInputElement;
expect(input).toHaveAttribute('step', '1');
fireEvent.change(input, { target: { value: '12:34:56' } });
const changed = onValueChange.mock.calls[0][0] as Date;
expect(changed.getMilliseconds()).toBe(0);
});
});
@@ -14,9 +14,17 @@
* limitations under the License.
*/
import { DateSystem, excelDateSerial, excelDateTimeSerial, excelSerialToDate } from '@univerjs/core';
import { describe, expect, it } from 'vitest';
import { stripErrorMargin } from '../../engine/utils/math-kit';
import { excelDateSerial, excelDateTimeSerial, excelSerialToDate, formatDateDefault, isValidDateStr } from '../date';
import { NumberValueObject } from '../../engine/value-object/primitive-object';
import {
formatDateDefault,
getDateSerialNumberByObject,
getTwoDateDaysByBasis,
isValidDateStr,
} from '../date';
import { ErrorType } from '../error-type';
describe('Test date', () => {
it('Function excelDateSerial', () => {
@@ -42,6 +50,8 @@ describe('Test date', () => {
expect(formatDateDefault(excelSerialToDate(61))).toBe('1900/03/01');
expect(formatDateDefault(excelSerialToDate(367))).toBe('1901/01/01');
expect(formatDateDefault(excelSerialToDate(45324))).toBe('2024/02/02');
expect(formatDateDefault(excelSerialToDate(0, DateSystem.Date1904))).toBe('1904/01/01');
expect(excelDateSerial(new Date(Date.UTC(1904, 0, 2)), DateSystem.Date1904)).toBe(1);
});
it('Function isValidDateStr', () => {
expect(isValidDateStr('2020-1-1')).toBeTruthy();
@@ -49,4 +59,20 @@ describe('Test date', () => {
expect(isValidDateStr('2020-2-31')).toBeFalsy();
expect(isValidDateStr('2020/001/31')).toBeFalsy();
});
it('uses the decoded 1904 date when day-count calculations receive serial zero', () => {
expect(getTwoDateDaysByBasis(0, 1, 0, DateSystem.Date1904).days).toBe(1);
expect(getTwoDateDaysByBasis(0, 1, 1, DateSystem.Date1904)).toEqual({ days: 1, yearDays: 366 });
expect(getTwoDateDaysByBasis(0, 1, 4, DateSystem.Date1904).days).toBe(1);
});
it('validates the maximum serial against the active Excel date system', () => {
const invalid1900 = getDateSerialNumberByObject(NumberValueObject.create(2958466), DateSystem.Date1900);
const invalid1904 = getDateSerialNumberByObject(NumberValueObject.create(2957004), DateSystem.Date1904);
expect(getDateSerialNumberByObject(NumberValueObject.create(2958465), DateSystem.Date1900)).toBe(2958465);
expect(typeof invalid1900 === 'number' ? invalid1900 : invalid1900.getValue()).toBe(ErrorType.NUM);
expect(getDateSerialNumberByObject(NumberValueObject.create(2957003), DateSystem.Date1904)).toBe(2957003);
expect(typeof invalid1904 === 'number' ? invalid1904 : invalid1904.getValue()).toBe(ErrorType.NUM);
});
});
@@ -14,6 +14,7 @@
* limitations under the License.
*/
import type { DateSystem } from '@univerjs/core';
import type { ArrayValueObject } from '../engine/value-object/array-value-object';
import type { BaseValueObject } from '../engine/value-object/base-value-object';
import { isRealNum } from '@univerjs/core';
@@ -212,7 +213,12 @@ export function checkCriteria(criteria: BaseValueObject) {
* Criteria match logic:
* Boolean AND in row direction, then Boolean OR in column direction
*/
export function isCriteriaMatch(criteria: DatabaseValueType[][], database: DatabaseValueType[][], databaseRowIndex: number) {
export function isCriteriaMatch(
criteria: DatabaseValueType[][],
database: DatabaseValueType[][],
databaseRowIndex: number,
dateSystem: DateSystem
) {
const rowCount = criteria.length;
const columnCount = criteria[0].length;
@@ -255,8 +261,8 @@ export function isCriteriaMatch(criteria: DatabaseValueType[][], database: Datab
break;
}
const [compareToken, criteriaObject] = findCompareToken(`${criteriaValue}`);
const compareObject = ValueObjectFactory.create(`${databaseValue}`).compare(criteriaObject, compareToken);
const [compareToken, criteriaObject] = findCompareToken(`${criteriaValue}`, dateSystem);
const compareObject = ValueObjectFactory.create(`${databaseValue}`, false, dateSystem).compare(criteriaObject, compareToken);
const compareValue = compareObject.getValue();
if (!compareValue) {
+89 -145
View File
@@ -15,7 +15,15 @@
*/
import type { BaseValueObject } from '../engine/value-object/base-value-object';
import { isRealNum, numfmt } from '@univerjs/core';
import {
DateSystem,
excelDateSerial,
excelDateTimeSerial,
excelSerialToDate,
getDateSystemMaxSerial,
isRealNum,
numfmt,
} from '@univerjs/core';
import { ErrorValueObject } from '../engine/value-object/base-value-object';
import { ErrorType } from './error-type';
@@ -23,84 +31,6 @@ export const DEFAULT_DATE_FORMAT = 'yyyy/mm/dd;@';
export const DEFAULT_NOW_FORMAT = 'yyyy/mm/dd hh:mm';
export const DEFAULT_TIME_FORMAT = 'h:mm A/P';
const perDayMilliseconds = 24 * 60 * 60 * 1000;
/**
* Excel stores dates as sequential serial numbers so they can be used in calculations. By default, January 1, 1900 is serial number 1, and January 1, 2008 is serial number 39448 because it is 39,447 days after January 1, 1900.
*
* Excel has a leap year error in 1900. February 29, 1900 is considered a legal date. In fact, there is no February 29 in 1900.
* 1900.2.28 Date Serial 59
* 1900.2.29 Date Serial 61
* 1900.3.1 Date Serial 61
* 1901.1.1 Date Serial 367
*/
export function excelDateSerial(date: Date): number {
const baseDateTime = Date.UTC(1900, 0, 1); // January 1, 1900, UTC
const leapDayDateTime = Date.UTC(1900, 1, 28); // February 28, 1900, UTC
const dateTime = Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()); // Input date at UTC
// Calculate the difference in days between the base date and the input date
let dayDifference = (dateTime - baseDateTime) / perDayMilliseconds;
// If the date is later than February 28, 1900, the day difference needs to be adjusted to account for Excel errors
if (dateTime > leapDayDateTime) {
dayDifference += 1;
}
return Math.floor(dayDifference) + 1; // Excel serial number starts from 1
}
/**
* Time serial number with date
*/
export function excelDateTimeSerial(date: Date): number {
const baseDateTime = Date.UTC(1900, 0, 1, 0, 0, 0); // January 1, 1900, UTC at midnight
const leapDayDateTime = Date.UTC(1900, 1, 28, 0, 0, 0); // February 28, 1900, UTC at midnight
const dateTime = Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(), date.getUTCHours(), date.getUTCMinutes(), date.getUTCSeconds(), date.getUTCMilliseconds()); // Input date at UTC midnight
// Calculate the difference in milliseconds between the input date and the base date
let dayDifference = (dateTime - baseDateTime) / perDayMilliseconds;
// Adjusting for the Excel leap year bug
if (dateTime > leapDayDateTime) {
dayDifference += 1;
}
return dayDifference + 1; // Excel serial number starts from 1
}
export function excelSerialToDate(serial: number): Date {
const baseDateTime = Date.UTC(1900, 0, 1); // January 1, 1900, UTC
const leapDayDateTime = Date.UTC(1900, 1, 28); // February 28, 1900, UTC
let dayDifference = Math.floor(serial) - 1; // Adjust for Excel serial number starting from 1
// If the serial number corresponds to a date later than February 28, 1900, adjust the day difference
if (dayDifference > (leapDayDateTime - baseDateTime) / perDayMilliseconds) {
dayDifference -= 1;
}
return new Date(baseDateTime + dayDifference * perDayMilliseconds);
}
export function excelSerialToDateTime(serial: number): Date {
const baseDateTime = Date.UTC(1900, 0, 1, 0, 0, 0); // January 1, 1900, UTC at midnight
const leapDayDateTime = Date.UTC(1900, 1, 28, 0, 0, 0); // February 28, 1900, UTC at midnight
let dayDifference = serial - 1; // Adjust for Excel serial number starting from 1
// If the serial number corresponds to a date later than February 28, 1900, adjust the day difference
if (dayDifference > (leapDayDateTime - baseDateTime) / perDayMilliseconds) {
dayDifference -= 1;
}
if (dayDifference < 0) {
dayDifference = serial;
}
return new Date(baseDateTime + dayDifference * perDayMilliseconds);
}
export function formatDateDefault(date: Date): string {
// Get the year from the date object
const year: number = date.getFullYear();
@@ -150,12 +80,12 @@ export function isValidDateStr(dateStr: string): boolean {
return dateStrPad === reconstructedDateStr;
}
export function parseFormattedDate(value: string) {
return numfmt.parseDate(value);
export function parseFormattedDate(value: string, options?: { dateSystem?: DateSystem }) {
return numfmt.parseDate(value, options);
}
export function parseFormattedTime(value: string) {
return numfmt.parseTime(value);
export function parseFormattedTime(value: string, options?: { dateSystem?: DateSystem }) {
return numfmt.parseTime(value, options);
}
function parseEnglishMonthNameDate(value: string): Date | null {
@@ -271,7 +201,7 @@ export function getWeekendArray(weekend: number | string): number[] {
return weekendNumberMap[Number(weekend)] || [];
}
export function countWorkingDays(startDateSerialNumber: number, endDateSerialNumber: number, weekend: number | string = 1, holidays?: number[]): number {
export function countWorkingDays(startDateSerialNumber: number, endDateSerialNumber: number, weekend: number | string = 1, holidays?: number[], dateSystem = DateSystem.Date1900): number {
const weekendArray = getWeekendArray(weekend);
const start = Math.floor(startDateSerialNumber);
@@ -289,7 +219,7 @@ export function countWorkingDays(startDateSerialNumber: number, endDateSerialNum
continue;
}
const weekDay = getWeekDayByDateSerialNumber(currentDateSerialNumber);
const weekDay = getWeekDayByDateSerialNumber(currentDateSerialNumber, dateSystem);
if (weekendArray.includes(weekDay)) {
continue;
@@ -301,7 +231,7 @@ export function countWorkingDays(startDateSerialNumber: number, endDateSerialNum
return end >= start ? workingDays : -workingDays;
}
export function getDateSerialNumberByWorkingDays(startDateSerialNumber: number, workingDays: number, weekend: number | string = 1, holidays?: number[]): (number | ErrorValueObject) {
export function getDateSerialNumberByWorkingDays(startDateSerialNumber: number, workingDays: number, weekend: number | string = 1, holidays?: number[], dateSystem = DateSystem.Date1900): number | ErrorValueObject {
const weekendArray = getWeekendArray(weekend);
const _startDateSerialNumber = Math.floor(startDateSerialNumber);
@@ -321,7 +251,7 @@ export function getDateSerialNumberByWorkingDays(startDateSerialNumber: number,
continue;
}
const weekDay = getWeekDayByDateSerialNumber(currentDateSerialNumber);
const weekDay = getWeekDayByDateSerialNumber(currentDateSerialNumber, dateSystem);
if (weekendArray.includes(weekDay)) {
days++;
@@ -334,20 +264,25 @@ export function getDateSerialNumberByWorkingDays(startDateSerialNumber: number,
return targetDateSerialNumber;
}
export function getDateSerialNumberByObject(serialNumberObject: BaseValueObject): (ErrorValueObject | number) {
export function getDateSerialNumberByObject(
serialNumberObject: BaseValueObject,
dateSystem?: DateSystem
): ErrorValueObject | number {
if (serialNumberObject.isError()) {
return serialNumberObject as ErrorValueObject;
}
const dateValue = serialNumberObject.getValue();
const maxSerial = getDateSystemMaxSerial(dateSystem);
if (serialNumberObject.isString()) {
let dateSerial: any;
const options = { dateSystem };
if (parseFormattedDate(`${dateValue}`)) {
dateSerial = parseFormattedDate(`${dateValue}`)!.v;
} else if (parseFormattedTime(`${dateValue}`)) {
dateSerial = parseFormattedTime(`${dateValue}`)!.v;
if (parseFormattedDate(`${dateValue}`, options)) {
dateSerial = parseFormattedDate(`${dateValue}`, options)!.v;
} else if (parseFormattedTime(`${dateValue}`, options)) {
dateSerial = parseFormattedTime(`${dateValue}`, options)!.v;
} else if (parseEnglishMonthNameDate(`${dateValue}`)) {
dateSerial = parseEnglishMonthNameDate(`${dateValue}`)!;
} else if (isRealNum(dateValue)) {
@@ -357,10 +292,10 @@ export function getDateSerialNumberByObject(serialNumberObject: BaseValueObject)
}
if (dateSerial instanceof Date) {
dateSerial = excelDateTimeSerial(dateSerial);
dateSerial = excelDateTimeSerial(dateSerial, dateSystem);
}
if (+dateSerial < 0 || +dateSerial > 2958465) { // 2958465 = 9999/12/31
if (+dateSerial < 0 || +dateSerial > maxSerial) {
return ErrorValueObject.create(ErrorType.NUM);
}
@@ -368,7 +303,7 @@ export function getDateSerialNumberByObject(serialNumberObject: BaseValueObject)
} else {
const dateSerial = +serialNumberObject.getValue();
if (dateSerial < 0 || dateSerial > 2958465) { // 2958465 = 9999/12/31
if (dateSerial < 0 || dateSerial > maxSerial) {
return ErrorValueObject.create(ErrorType.NUM);
}
@@ -376,20 +311,9 @@ export function getDateSerialNumberByObject(serialNumberObject: BaseValueObject)
}
}
export function getWeekDayByDateSerialNumber(dateSerialNumber: number): number {
// special date 1900-02-29(serialNumber = 60)
const isDate19000229 = Math.floor(dateSerialNumber) === 60;
let date = excelSerialToDate(dateSerialNumber);
const leapDayDateTime = Date.UTC(1900, 1, 28); // February 28, 1900, UTC
const dateTime = Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate());
if (!isDate19000229 && dateTime <= leapDayDateTime) {
date = new Date(dateTime - perDayMilliseconds);
}
return date.getUTCDay();
export function getWeekDayByDateSerialNumber(dateSerialNumber: number, dateSystem = DateSystem.Date1900): number {
const offset = dateSystem === DateSystem.Date1904 ? 5 : 6;
return ((Math.floor(dateSerialNumber) + offset) % 7 + 7) % 7;
}
interface ITwoDateDaysType {
@@ -397,14 +321,14 @@ interface ITwoDateDaysType {
yearDays: number;
}
export function getTwoDateDaysByBasis(startDateSerialNumber: number, endDateSerialNumber: number, basis: number): ITwoDateDaysType {
export function getTwoDateDaysByBasis(startDateSerialNumber: number, endDateSerialNumber: number, basis: number, dateSystem = DateSystem.Date1900): ITwoDateDaysType {
switch (basis) {
case 0:
// U.S. (NASD) method 30/360.
return getDaysByNASD(startDateSerialNumber, endDateSerialNumber);
return getDaysByNASD(startDateSerialNumber, endDateSerialNumber, dateSystem);
case 1:
// Actual/actual
return getDaysByActual(startDateSerialNumber, endDateSerialNumber);
return getDaysByActual(startDateSerialNumber, endDateSerialNumber, dateSystem);
case 2:
// Actual/360
return {
@@ -419,7 +343,7 @@ export function getTwoDateDaysByBasis(startDateSerialNumber: number, endDateSeri
};
case 4:
// European method 30/360.
return getDaysByEuropean(startDateSerialNumber, endDateSerialNumber);
return getDaysByEuropean(startDateSerialNumber, endDateSerialNumber, dateSystem);
default:
return {
days: Math.abs(endDateSerialNumber - startDateSerialNumber),
@@ -428,16 +352,18 @@ export function getTwoDateDaysByBasis(startDateSerialNumber: number, endDateSeri
}
}
function getDaysByNASD(startDateSerialNumber: number, endDateSerialNumber: number): ITwoDateDaysType {
const startDateDate = excelSerialToDate(startDateSerialNumber);
const startYear = startDateSerialNumber > 0 ? startDateDate.getUTCFullYear() : 1900;
const startMonth = startDateSerialNumber > 0 ? startDateDate.getUTCMonth() + 1 : 1;
let startDay = startDateSerialNumber > 0 ? startDateDate.getUTCDate() : 0;
function getDaysByNASD(startDateSerialNumber: number, endDateSerialNumber: number, dateSystem: DateSystem): ITwoDateDaysType {
const startDateDate = excelSerialToDate(startDateSerialNumber, dateSystem);
const startIsDate1900SerialZero = dateSystem === DateSystem.Date1900 && startDateSerialNumber === 0;
const startYear = startIsDate1900SerialZero ? 1900 : startDateDate.getUTCFullYear();
const startMonth = startIsDate1900SerialZero ? 1 : startDateDate.getUTCMonth() + 1;
let startDay = startIsDate1900SerialZero ? 0 : startDateDate.getUTCDate();
let endDateDate = excelSerialToDate(endDateSerialNumber);
let endYear = endDateSerialNumber > 0 ? endDateDate.getUTCFullYear() : 1900;
let endMonth = endDateSerialNumber > 0 ? endDateDate.getUTCMonth() + 1 : 1;
let endDay = endDateSerialNumber > 0 ? endDateDate.getUTCDate() : 0;
let endDateDate = excelSerialToDate(endDateSerialNumber, dateSystem);
const endIsDate1900SerialZero = dateSystem === DateSystem.Date1900 && endDateSerialNumber === 0;
let endYear = endIsDate1900SerialZero ? 1900 : endDateDate.getUTCFullYear();
let endMonth = endIsDate1900SerialZero ? 1 : endDateDate.getUTCMonth() + 1;
let endDay = endIsDate1900SerialZero ? 0 : endDateDate.getUTCDate();
const startIsLastDayOfFebruary = startMonth === 2 && startDay === getDaysInMonth(startYear, startMonth - 1);
@@ -453,7 +379,7 @@ function getDaysByNASD(startDateSerialNumber: number, endDateSerialNumber: numbe
endDay = 30;
} else if (endDay === 31) {
if (startDay < 30) {
endDateDate = excelSerialToDate(endDateSerialNumber + 1);
endDateDate = excelSerialToDate(endDateSerialNumber + 1, dateSystem);
endYear = endDateDate.getUTCFullYear();
endMonth = endDateDate.getUTCMonth() + 1;
endDay = endDateDate.getUTCDate();
@@ -474,14 +400,30 @@ function getDaysByNASD(startDateSerialNumber: number, endDateSerialNumber: numbe
};
}
function getDaysByActual(startDateSerialNumber: number, endDateSerialNumber: number): ITwoDateDaysType {
const startDateDate = excelSerialToDate(startDateSerialNumber);
const startYear = startDateSerialNumber > 0 ? startDateDate.getUTCFullYear() : 1900;
function getDaysByActual(startDateSerialNumber: number, endDateSerialNumber: number, dateSystem: DateSystem): ITwoDateDaysType {
const startDateDate = excelSerialToDate(startDateSerialNumber, dateSystem);
const startIsDate1900SerialZero = dateSystem === DateSystem.Date1900 && startDateSerialNumber === 0;
const startYear = startIsDate1900SerialZero ? 1900 : startDateDate.getUTCFullYear();
const startMonth = startIsDate1900SerialZero ? 1 : startDateDate.getUTCMonth() + 1;
const startDay = startIsDate1900SerialZero ? 0 : startDateDate.getUTCDate();
const endDateDate = excelSerialToDate(endDateSerialNumber);
const endYear = endDateSerialNumber > 0 ? endDateDate.getUTCFullYear() : 1900;
const endDateDate = excelSerialToDate(endDateSerialNumber, dateSystem);
const endIsDate1900SerialZero = dateSystem === DateSystem.Date1900 && endDateSerialNumber === 0;
const endYear = endIsDate1900SerialZero ? 1900 : endDateDate.getUTCFullYear();
const endMonth = endIsDate1900SerialZero ? 1 : endDateDate.getUTCMonth() + 1;
const endDay = endIsDate1900SerialZero ? 0 : endDateDate.getUTCDate();
const totalDays = Math.abs(endDateSerialNumber - startDateSerialNumber);
// A period from a date to the same date in a later year is a whole number of years.
// Excel uses the actual length of those years instead of averaging both boundary years.
if (endDateSerialNumber >= startDateSerialNumber && endYear > startYear && startMonth === endMonth && startDay === endDay) {
return {
days: totalDays,
yearDays: totalDays / (endYear - startYear),
};
}
const totalYear = Math.abs(endYear - startYear) + 1;
let startYearFirstDaySerialNumber;
@@ -491,20 +433,20 @@ function getDaysByActual(startDateSerialNumber: number, endDateSerialNumber: num
const startYearFirstDay = new Date(Date.UTC(endYear, 0, 1));
const endYearLastDay = new Date(Date.UTC(startYear, 11, 31));
startYearFirstDaySerialNumber = excelDateSerial(startYearFirstDay);
endYearLastDaySerialNumber = excelDateSerial(endYearLastDay);
startYearFirstDaySerialNumber = excelDateSerial(startYearFirstDay, dateSystem);
endYearLastDaySerialNumber = excelDateSerial(endYearLastDay, dateSystem);
if (endYear === 1900) { // Special handle. excel 1900 days = 365, 1900/12/31 SerialNumber = 366. so start add 1
if (dateSystem === DateSystem.Date1900 && endYear === 1900) { // Special handle. excel 1900 days = 365, 1900/12/31 SerialNumber = 366. so start add 1
startYearFirstDaySerialNumber += 1;
}
} else {
const startYearFirstDay = new Date(Date.UTC(startYear, 0, 1));
const endYearLastDay = new Date(Date.UTC(endYear, 11, 31));
startYearFirstDaySerialNumber = excelDateSerial(startYearFirstDay);
endYearLastDaySerialNumber = excelDateSerial(endYearLastDay);
startYearFirstDaySerialNumber = excelDateSerial(startYearFirstDay, dateSystem);
endYearLastDaySerialNumber = excelDateSerial(endYearLastDay, dateSystem);
if (startYear === 1900) { // Special handle. excel 1900 days = 365, 1900/12/31 SerialNumber = 366. so start add 1
if (dateSystem === DateSystem.Date1900 && startYear === 1900) { // Special handle. excel 1900 days = 365, 1900/12/31 SerialNumber = 366. so start add 1
startYearFirstDaySerialNumber += 1;
}
}
@@ -515,16 +457,18 @@ function getDaysByActual(startDateSerialNumber: number, endDateSerialNumber: num
};
}
function getDaysByEuropean(startDateSerialNumber: number, endDateSerialNumber: number): ITwoDateDaysType {
const startDateDate = excelSerialToDate(startDateSerialNumber);
const startYear = startDateSerialNumber > 0 ? startDateDate.getUTCFullYear() : 1900;
const startMonth = startDateSerialNumber > 0 ? startDateDate.getUTCMonth() + 1 : 1;
let startDay = startDateSerialNumber > 0 ? startDateDate.getUTCDate() : 0;
function getDaysByEuropean(startDateSerialNumber: number, endDateSerialNumber: number, dateSystem: DateSystem): ITwoDateDaysType {
const startDateDate = excelSerialToDate(startDateSerialNumber, dateSystem);
const startIsDate1900SerialZero = dateSystem === DateSystem.Date1900 && startDateSerialNumber === 0;
const startYear = startIsDate1900SerialZero ? 1900 : startDateDate.getUTCFullYear();
const startMonth = startIsDate1900SerialZero ? 1 : startDateDate.getUTCMonth() + 1;
let startDay = startIsDate1900SerialZero ? 0 : startDateDate.getUTCDate();
const endDateDate = excelSerialToDate(endDateSerialNumber);
const endYear = endDateSerialNumber > 0 ? endDateDate.getUTCFullYear() : 1900;
const endMonth = endDateSerialNumber > 0 ? endDateDate.getUTCMonth() + 1 : 1;
let endDay = endDateSerialNumber > 0 ? endDateDate.getUTCDate() : 0;
const endDateDate = excelSerialToDate(endDateSerialNumber, dateSystem);
const endIsDate1900SerialZero = dateSystem === DateSystem.Date1900 && endDateSerialNumber === 0;
const endYear = endIsDate1900SerialZero ? 1900 : endDateDate.getUTCFullYear();
const endMonth = endIsDate1900SerialZero ? 1 : endDateDate.getUTCMonth() + 1;
let endDay = endIsDate1900SerialZero ? 0 : endDateDate.getUTCDate();
// Starting dates and ending dates that occur on the 31st day of a month become equal to the 30th day of the same month.
if (startDay === 31) {
@@ -567,14 +511,14 @@ export function getDaysInYear(year: number): number {
return isLeapYear1900(year) ? 366 : 365;
}
export function getNormalYearDaysByBasis(dateSerialNumber: number, basis: number): number {
export function getNormalYearDaysByBasis(dateSerialNumber: number, basis: number, dateSystem = DateSystem.Date1900): number {
switch (basis) {
case 0:
case 2:
case 4:
return 360;
case 1:
return getDaysInYear(excelSerialToDate(dateSerialNumber).getUTCFullYear());
return getDaysInYear(excelSerialToDate(dateSerialNumber, dateSystem).getUTCFullYear());
case 3:
return 365;
default:
+182 -104
View File
@@ -14,39 +14,153 @@
* limitations under the License.
*/
import { DateSystem, excelDateSerial, excelDateTimePartsToSerial, excelSerialToDate, excelSerialToDateTimeParts } from '@univerjs/core';
import { ErrorValueObject } from '../engine/value-object/base-value-object';
import {
dateAddMonths,
excelDateSerial,
excelSerialToDate,
getDaysInMonth,
getTwoDateDaysByBasis,
lastDayOfMonth,
} from './date';
import { ErrorType } from './error-type';
export function calculateCoupdaybs(settlementSerialNumber: number, maturitySerialNumber: number, frequency: number, basis: number): number {
const coupDateSerialNumber = calculateCouppcd(settlementSerialNumber, maturitySerialNumber, frequency);
interface ICouponDateParts {
year: number;
month: number;
day: number;
monthEnd: boolean;
}
const { days } = getTwoDateDaysByBasis(coupDateSerialNumber, settlementSerialNumber, basis);
interface ICouponSchedule {
previousSerial: number;
nextSerial: number;
count: number;
}
const COUPON_DATE_PARTS_OPTIONS = { hours: 0, minutes: 0, seconds: 0, fractionalSecond: 0 };
function getCouponDateParts(serial: number, dateSystem: DateSystem): ICouponDateParts {
const parts = excelSerialToDateTimeParts(serial, { dateSystem });
if (parts) {
return {
year: parts.year,
month: parts.month,
day: parts.day,
monthEnd: parts.day === getDaysInMonth(parts.year, parts.month - 1),
};
}
const date = excelSerialToDate(serial, dateSystem);
const year = date.getUTCFullYear();
const month = date.getUTCMonth() + 1;
const day = date.getUTCDate();
return { year, month, day, monthEnd: day === getDaysInMonth(year, month - 1) };
}
function getCouponDateSerial(parts: ICouponDateParts, dateSystem: DateSystem): number {
if (dateSystem === DateSystem.Date1900 && parts.year === 1900 && parts.month === 1 && parts.day === 0) {
return 0;
}
if (dateSystem === DateSystem.Date1900 && parts.year === 1900 && parts.month === 2 && parts.day === 29) {
return 60;
}
const serial = excelDateTimePartsToSerial({
year: parts.year,
month: parts.month,
day: parts.day,
...COUPON_DATE_PARTS_OPTIONS,
}, { dateSystem });
if (serial != null) {
return serial;
}
return excelDateSerial(new Date(Date.UTC(parts.year, parts.month - 1, parts.day)), dateSystem);
}
function compareCouponDate(left: ICouponDateParts, right: ICouponDateParts): number {
if (left.year !== right.year) {
return left.year - right.year;
}
if (left.month !== right.month) {
return left.month - right.month;
}
return left.day - right.day;
}
// Excel keeps the maturity day/month-end rule for every coupon period; it must not be inferred from an intermediate date.
function addCouponMonths(parts: ICouponDateParts, months: number): ICouponDateParts {
const monthIndex = parts.year * 12 + parts.month - 1 + months;
const year = Math.floor(monthIndex / 12);
const month = monthIndex - year * 12 + 1;
const maxDay = getDaysInMonth(year, month - 1);
const day = parts.monthEnd ? maxDay : Math.min(parts.day, maxDay);
return { year, month, day, monthEnd: parts.monthEnd };
}
function getCouponSchedule(
settlementSerialNumber: number,
maturitySerialNumber: number,
frequency: number,
dateSystem: DateSystem
): ICouponSchedule {
const settlement = getCouponDateParts(settlementSerialNumber, dateSystem);
const maturity = getCouponDateParts(maturitySerialNumber, dateSystem);
const periodMonths = 12 / frequency;
let candidate: ICouponDateParts = {
...maturity,
year: settlement.year,
};
if (compareCouponDate(candidate, settlement) < 0) {
candidate = { ...candidate, year: candidate.year + 1 };
}
while (compareCouponDate(candidate, settlement) > 0) {
candidate = addCouponMonths(candidate, -periodMonths);
}
const previousSerial = getCouponDateSerial(candidate, dateSystem);
const nextSerial = getCouponDateSerial(addCouponMonths(candidate, periodMonths), dateSystem);
let count = 0;
let countCandidate = maturity;
while (compareCouponDate(countCandidate, settlement) > 0) {
countCandidate = addCouponMonths(countCandidate, -periodMonths);
count++;
}
return { previousSerial, nextSerial, count };
}
export function calculateCoupdaybs(settlementSerialNumber: number, maturitySerialNumber: number, frequency: number, basis: number, dateSystem = DateSystem.Date1900): number {
const coupDateSerialNumber = calculateCouppcd(settlementSerialNumber, maturitySerialNumber, frequency, dateSystem);
const { days } = getTwoDateDaysByBasis(coupDateSerialNumber, settlementSerialNumber, basis, dateSystem);
return days;
}
export function calculateCoupdays(settlementSerialNumber: number, maturitySerialNumber: number, frequency: number, basis: number): number {
export function calculateCoupdays(settlementSerialNumber: number, maturitySerialNumber: number, frequency: number, basis: number, dateSystem = DateSystem.Date1900): number {
let result;
if (basis === 1) {
const beforeSettlementDateSerialNumber = calculateCouppcd(settlementSerialNumber, maturitySerialNumber, frequency);
const beforeSettlementDateSerialNumber = calculateCouppcd(settlementSerialNumber, maturitySerialNumber, frequency, dateSystem);
let coupDate = excelSerialToDate(beforeSettlementDateSerialNumber);
coupDate = dateAddMonths(coupDate, 12 / frequency);
const afterSettlementDateSerialNumber = excelDateSerial(coupDate);
const afterSettlementDateSerialNumber = getCouponSchedule(settlementSerialNumber, maturitySerialNumber, frequency, dateSystem).nextSerial;
// special handle for excel
if (beforeSettlementDateSerialNumber < 0 && frequency === 1) {
result = 365;
} else if (dateSystem === DateSystem.Date1900 && settlementSerialNumber === 0 && frequency === 2 && beforeSettlementDateSerialNumber < 0) {
result = 184;
} else {
result = afterSettlementDateSerialNumber - beforeSettlementDateSerialNumber;
}
@@ -59,64 +173,22 @@ export function calculateCoupdays(settlementSerialNumber: number, maturitySerial
return result;
}
export function calculateCoupncd(settlementSerialNumber: number, maturitySerialNumber: number, frequency: number): number {
const settlementDate = excelSerialToDate(settlementSerialNumber);
let coupDate = excelSerialToDate(maturitySerialNumber);
coupDate.setUTCFullYear(settlementDate.getUTCFullYear());
if (coupDate < settlementDate) {
coupDate.setUTCFullYear(coupDate.getUTCFullYear() + 1);
}
while (coupDate > settlementDate) {
coupDate = dateAddMonths(coupDate, -12 / frequency);
}
coupDate = dateAddMonths(coupDate, 12 / frequency);
const coupDateSerialNumber = excelDateSerial(coupDate);
return coupDateSerialNumber;
export function calculateCoupncd(settlementSerialNumber: number, maturitySerialNumber: number, frequency: number, dateSystem = DateSystem.Date1900): number {
return getCouponSchedule(settlementSerialNumber, maturitySerialNumber, frequency, dateSystem).nextSerial;
}
export function calculateCoupnum(settlementSerialNumber: number, maturitySerialNumber: number, frequency: number): number {
let result = 0;
const settlementDate = excelSerialToDate(settlementSerialNumber);
let coupDate = excelSerialToDate(maturitySerialNumber);
while (coupDate > settlementDate) {
coupDate = dateAddMonths(coupDate, -12 / frequency);
result++;
}
return result;
export function calculateCoupnum(settlementSerialNumber: number, maturitySerialNumber: number, frequency: number, dateSystem = DateSystem.Date1900): number {
return getCouponSchedule(settlementSerialNumber, maturitySerialNumber, frequency, dateSystem).count;
}
export function calculateCouppcd(settlementSerialNumber: number, maturitySerialNumber: number, frequency: number): number {
const settlementDate = excelSerialToDate(settlementSerialNumber);
let coupDate = excelSerialToDate(maturitySerialNumber);
coupDate.setUTCFullYear(settlementDate.getUTCFullYear());
if (coupDate < settlementDate) {
coupDate.setUTCFullYear(coupDate.getUTCFullYear() + 1);
}
while (coupDate > settlementDate) {
coupDate = dateAddMonths(coupDate, -12 / frequency);
}
const coupDateSerialNumber = excelDateSerial(coupDate);
return coupDateSerialNumber;
export function calculateCouppcd(settlementSerialNumber: number, maturitySerialNumber: number, frequency: number, dateSystem = DateSystem.Date1900): number {
return getCouponSchedule(settlementSerialNumber, maturitySerialNumber, frequency, dateSystem).previousSerial;
}
export function calculateDuration(settlementSerialNumber: number, maturitySerialNumber: number, coupon: number, yld: number, frequency: number, basis: number): number {
const coupdaybs = calculateCoupdaybs(settlementSerialNumber, maturitySerialNumber, frequency, basis);
const coupdays = calculateCoupdays(settlementSerialNumber, maturitySerialNumber, frequency, basis);
const coupnum = calculateCoupnum(settlementSerialNumber, maturitySerialNumber, frequency);
export function calculateDuration(settlementSerialNumber: number, maturitySerialNumber: number, coupon: number, yld: number, frequency: number, basis: number, dateSystem = DateSystem.Date1900): number {
const coupdaybs = calculateCoupdaybs(settlementSerialNumber, maturitySerialNumber, frequency, basis, dateSystem);
const coupdays = calculateCoupdays(settlementSerialNumber, maturitySerialNumber, frequency, basis, dateSystem);
const coupnum = calculateCoupnum(settlementSerialNumber, maturitySerialNumber, frequency, dateSystem);
const coupdaysDiff = (coupdays - coupdaybs) / coupdays - 1;
const _yld = yld / frequency + 1;
@@ -213,13 +285,14 @@ export function calculateOddFPrice(
yld: number,
redemption: number,
frequency: number,
basis: number
basis: number,
dateSystem = DateSystem.Date1900
): number {
// DFC = number of days from the beginning of the odd first coupon to the first coupon date.
const DFC = getPositiveDaysBetween(issueSerialNumber, firstCouponSerialNumber, basis);
const DFC = getPositiveDaysBetween(issueSerialNumber, firstCouponSerialNumber, basis, dateSystem);
// E = number of days in the coupon period.
const E = calculateCoupdays(settlementSerialNumber, firstCouponSerialNumber, frequency, basis);
const E = calculateCoupdays(settlementSerialNumber, firstCouponSerialNumber, frequency, basis, dateSystem);
if (DFC < E) {
return calculateOddShortFirstCoupon(
@@ -233,7 +306,8 @@ export function calculateOddFPrice(
frequency,
basis,
DFC,
E
E,
dateSystem
);
} else {
return calculateOddLongFirstCoupon(
@@ -246,7 +320,8 @@ export function calculateOddFPrice(
redemption,
frequency,
basis,
E
E,
dateSystem
);
}
}
@@ -262,7 +337,8 @@ function calculateOddShortFirstCoupon(
frequency: number,
basis: number,
DFC: number,
E: number
E: number,
dateSystem: DateSystem
): number {
// calculate method from
// https://support.microsoft.com/en-us/office/oddfprice-function-d7d664a8-34df-4233-8d2b-922bcf6a69e1
@@ -270,10 +346,10 @@ function calculateOddShortFirstCoupon(
let result = 0;
// N = number of coupons payable between the settlement date and the redemption date. (If this number contains a fraction, it is raised to the next whole number.)
const N = calculateCoupnum(settlementSerialNumber, maturitySerialNumber, frequency);
const N = calculateCoupnum(settlementSerialNumber, maturitySerialNumber, frequency, dateSystem);
// DSC = number of days from the settlement to the next coupon date.
const DSC = getPositiveDaysBetween(settlementSerialNumber, firstCouponSerialNumber, basis);
const DSC = getPositiveDaysBetween(settlementSerialNumber, firstCouponSerialNumber, basis, dateSystem);
result += redemption / ((1 + yld / frequency) ** (N - 1 + DSC / E));
@@ -284,7 +360,7 @@ function calculateOddShortFirstCoupon(
}
// A = number of days from the beginning of the coupon period to the settlement date (accrued days).
const A = getPositiveDaysBetween(issueSerialNumber, settlementSerialNumber, basis);
const A = getPositiveDaysBetween(issueSerialNumber, settlementSerialNumber, basis, dateSystem);
result -= 100 * rate / frequency * A / E;
@@ -301,7 +377,8 @@ function calculateOddLongFirstCoupon(
redemption: number,
frequency: number,
basis: number,
E: number
E: number,
dateSystem: DateSystem
): number {
// calculate method from
// https://support.microsoft.com/en-us/office/oddfprice-function-d7d664a8-34df-4233-8d2b-922bcf6a69e1
@@ -309,42 +386,42 @@ function calculateOddLongFirstCoupon(
let result = 0;
// N = number of coupons payable between the first real coupon date and redemption date. (If this number contains a fraction, it is raised to the next whole number.)
const N = calculateCoupnum(firstCouponSerialNumber, maturitySerialNumber, frequency);
const N = calculateCoupnum(firstCouponSerialNumber, maturitySerialNumber, frequency, dateSystem);
// Nq = number of whole quasi-coupon periods between settlement date and first coupon.
const Nq = getCouponsNumber(firstCouponSerialNumber, settlementSerialNumber, 12 / frequency, true);
const Nq = getCouponsNumber(firstCouponSerialNumber, settlementSerialNumber, 12 / frequency, true, dateSystem);
// DSC = number of days from the settlement to the next coupon date.
let DSC;
if (basis === 2 || basis === 3) {
const coupncd = calculateCoupncd(settlementSerialNumber, firstCouponSerialNumber, frequency);
DSC = getPositiveDaysBetween(settlementSerialNumber, coupncd, basis);
const coupncd = calculateCoupncd(settlementSerialNumber, firstCouponSerialNumber, frequency, dateSystem);
DSC = getPositiveDaysBetween(settlementSerialNumber, coupncd, basis, dateSystem);
} else {
const couppcd = calculateCouppcd(settlementSerialNumber, firstCouponSerialNumber, frequency);
const { days } = getTwoDateDaysByBasis(couppcd, settlementSerialNumber, basis);
const couppcd = calculateCouppcd(settlementSerialNumber, firstCouponSerialNumber, frequency, dateSystem);
const { days } = getTwoDateDaysByBasis(couppcd, settlementSerialNumber, basis, dateSystem);
DSC = E - days;
}
result += redemption / ((1 + yld / frequency) ** (N + Nq + DSC / E));
// NC = number of quasi-coupon periods that fit in odd period. (If this number contains a fraction, it is raised to the next whole number.)
const NC = calculateCoupnum(issueSerialNumber, firstCouponSerialNumber, frequency);
const NC = calculateCoupnum(issueSerialNumber, firstCouponSerialNumber, frequency, dateSystem);
let lateCoupon = firstCouponSerialNumber;
let DCiDivNLiSum = 0;
let AiDivNLiSum = 0;
for (let index = NC; index >= 1; index--) {
const earlyCoupon = getDateSerialNumberByMonths(lateCoupon, -12 / frequency, false);
const NLi = basis === 1 ? getPositiveDaysBetween(earlyCoupon, lateCoupon, basis) : E;
const DCi = index > 1 ? NLi : getPositiveDaysBetween(issueSerialNumber, lateCoupon, basis);
const earlyCoupon = getDateSerialNumberByMonths(lateCoupon, -12 / frequency, false, dateSystem);
const NLi = basis === 1 ? getPositiveDaysBetween(earlyCoupon, lateCoupon, basis, dateSystem) : E;
const DCi = index > 1 ? NLi : getPositiveDaysBetween(issueSerialNumber, lateCoupon, basis, dateSystem);
DCiDivNLiSum += DCi / NLi;
const startDate = issueSerialNumber > earlyCoupon ? issueSerialNumber : earlyCoupon;
const endDate = settlementSerialNumber < lateCoupon ? settlementSerialNumber : lateCoupon;
const Ai = getPositiveDaysBetween(startDate, endDate, basis);
const Ai = getPositiveDaysBetween(startDate, endDate, basis, dateSystem);
AiDivNLiSum += Ai / NLi;
@@ -362,20 +439,20 @@ function calculateOddLongFirstCoupon(
return result;
}
function getPositiveDaysBetween(startDateSerialNumber: number, endDateSerialNumber: number, basis: number): number {
const { days } = getTwoDateDaysByBasis(startDateSerialNumber, endDateSerialNumber, basis);
function getPositiveDaysBetween(startDateSerialNumber: number, endDateSerialNumber: number, basis: number, dateSystem: DateSystem): number {
const { days } = getTwoDateDaysByBasis(startDateSerialNumber, endDateSerialNumber, basis, dateSystem);
return startDateSerialNumber < endDateSerialNumber ? days : 0;
}
export function validDaysBetweenIsWholeFrequencyByTwoDate(date1SerialNumber: number, date2SerialNumber: number, frequency: number): boolean {
const date1 = excelSerialToDate(date1SerialNumber);
export function validDaysBetweenIsWholeFrequencyByTwoDate(date1SerialNumber: number, date2SerialNumber: number, frequency: number, dateSystem = DateSystem.Date1900): boolean {
const date1 = excelSerialToDate(date1SerialNumber, dateSystem);
const date1Year = date1.getUTCFullYear();
const date1Month = date1.getUTCMonth();
const date1Day = date1.getUTCDate();
const date1LastDayOfMonth = lastDayOfMonth(date1Year, date1Month, date1Day);
const date2 = excelSerialToDate(date2SerialNumber);
const date2 = excelSerialToDate(date2SerialNumber, dateSystem);
const date2Year = date2.getUTCFullYear();
const date2Month = date2.getUTCMonth();
const date2Day = date2.getUTCDate();
@@ -394,13 +471,13 @@ export function validDaysBetweenIsWholeFrequencyByTwoDate(date1SerialNumber: num
return true;
}
export function validCouppcdIsGte0ByTwoDate(date1SerialNumber: number, date2SerialNumber: number, frequency: number): boolean {
const couppcd = calculateCouppcd(date1SerialNumber, date2SerialNumber, frequency);
export function validCouppcdIsGte0ByTwoDate(date1SerialNumber: number, date2SerialNumber: number, frequency: number, dateSystem = DateSystem.Date1900): boolean {
const couppcd = calculateCouppcd(date1SerialNumber, date2SerialNumber, frequency, dateSystem);
return couppcd >= 0;
}
export function getDateSerialNumberByMonths(serialNumber: number, months: number, returnLastDay: boolean): number {
let date = excelSerialToDate(serialNumber);
export function getDateSerialNumberByMonths(serialNumber: number, months: number, returnLastDay: boolean, dateSystem = DateSystem.Date1900): number {
let date = excelSerialToDate(serialNumber, dateSystem);
date = dateAddMonths(date, months);
if (returnLastDay) {
@@ -410,12 +487,12 @@ export function getDateSerialNumberByMonths(serialNumber: number, months: number
date.setUTCDate(daysInMonth);
}
return excelDateSerial(date);
return excelDateSerial(date, dateSystem);
}
function getCouponsNumber(startDateSerialNumber: number, endDateSerialNumber: number, months: number, isWholeNumber: boolean): number {
const startDate = excelSerialToDate(startDateSerialNumber);
const endDate = excelSerialToDate(endDateSerialNumber);
function getCouponsNumber(startDateSerialNumber: number, endDateSerialNumber: number, months: number, isWholeNumber: boolean, dateSystem: DateSystem): number {
const startDate = excelSerialToDate(startDateSerialNumber, dateSystem);
const endDate = excelSerialToDate(endDateSerialNumber, dateSystem);
const startDateYear = startDate.getUTCFullYear();
const startDateMonth = startDate.getUTCMonth();
@@ -430,13 +507,13 @@ function getCouponsNumber(startDateSerialNumber: number, endDateSerialNumber: nu
? lastDayOfMonth(endDateYear, endDateMonth, endDateDay)
: endOfMonthTemp;
const newDateSerialNumber = getDateSerialNumberByMonths(endDateSerialNumber, 0, endOfMonth);
const newDateSerialNumber = getDateSerialNumberByMonths(endDateSerialNumber, 0, endOfMonth, dateSystem);
let coupons = (+isWholeNumber - 0) + +(endDateSerialNumber < newDateSerialNumber);
let frontDateSerialNumber = getDateSerialNumberByMonths(newDateSerialNumber, months, endOfMonth);
let frontDateSerialNumber = getDateSerialNumberByMonths(newDateSerialNumber, months, endOfMonth, dateSystem);
while (!(months > 0 ? frontDateSerialNumber >= endDateSerialNumber : frontDateSerialNumber <= endDateSerialNumber)) {
frontDateSerialNumber = getDateSerialNumberByMonths(frontDateSerialNumber, months, endOfMonth);
frontDateSerialNumber = getDateSerialNumberByMonths(frontDateSerialNumber, months, endOfMonth, dateSystem);
coupons++;
}
@@ -551,16 +628,17 @@ export function calculatePrice(
yld: number,
redemption: number,
frequency: number,
basis: number
basis: number,
dateSystem = DateSystem.Date1900
): number {
// N is the number of coupons payable between the settlement date and redemption date
const N = calculateCoupnum(settlementSerialNumber, maturitySerialNumber, frequency);
const N = calculateCoupnum(settlementSerialNumber, maturitySerialNumber, frequency, dateSystem);
// E = number of days in coupon period in which the settlement date falls.
const E = calculateCoupdays(settlementSerialNumber, maturitySerialNumber, frequency, basis);
const E = calculateCoupdays(settlementSerialNumber, maturitySerialNumber, frequency, basis, dateSystem);
// A = number of days from beginning of coupon period to settlement date.
const A = calculateCoupdaybs(settlementSerialNumber, maturitySerialNumber, frequency, basis);
const A = calculateCoupdaybs(settlementSerialNumber, maturitySerialNumber, frequency, basis, dateSystem);
if (N === 1) {
const DSR = E - A;
+8 -8
View File
@@ -14,20 +14,20 @@
* limitations under the License.
*/
import type { DateSystem } from '@univerjs/core';
import type { BaseValueObject } from '../engine/value-object/base-value-object';
import { numfmt } from '@univerjs/core';
import { stripErrorMargin } from '../engine/utils/math-kit';
/**
* covert number to preview string by pattern
* @TODODushusir: Internationalization, reuse with numfmt
* Convert a number to a preview string using the specified pattern.
*
* @param pattern
* @param value
* @returns
* @param pattern Number format pattern.
* @param value Number to format.
* @returns Formatted preview string.
*/
export const getFormatPreview = (pattern: string, value: number) => {
return numfmt.format(pattern, value, { throws: false });
export const getFormatPreview = (pattern: string, value: number, dateSystem?: DateSystem) => {
return numfmt.format(pattern, value, { dateSystem, throws: false });
};
export const getTextValueOfNumberFormat = (text: BaseValueObject): string => {
@@ -43,7 +43,7 @@ export const getTextValueOfNumberFormat = (text: BaseValueObject): string => {
if (text.isNumber()) {
if (text.getPattern() !== '') {
textValue = getFormatPreview(text.getPattern(), +text.getValue());
textValue = getFormatPreview(text.getPattern(), +text.getValue(), text.getDateSystem());
} else {
// Specify Number.EPSILON to not discard necessary digits in the case of non-precision errors, for example, the length of 1/3 is 17
textValue = `${stripErrorMargin(+text.getValue())}`;
@@ -0,0 +1,248 @@
/**
* 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 { IWorkbookData, Workbook } from '@univerjs/core';
import type { LambdaValueObjectObject } from '../../value-object/lambda-value-object';
import { DateSystem, LocaleType, UniverInstanceType } from '@univerjs/core';
import { describe, expect, it } from 'vitest';
import { ErrorType } from '../../../basics/error-type';
import { createFunctionTestBed } from '../../../functions/__tests__/create-function-test-bed';
import { AsyncCustomFunction, CustomFunction } from '../../../functions/custom-function';
import { DateFunction } from '../../../functions/date/date';
import { FUNCTION_NAMES_DATE } from '../../../functions/date/function-names';
import { Accrint } from '../../../functions/financial/accrint';
import { FUNCTION_NAMES_FINANCIAL } from '../../../functions/financial/function-names';
import { Abs } from '../../../functions/math/abs';
import { FUNCTION_NAMES_MATH } from '../../../functions/math/function-names';
import { FUNCTION_NAMES_META } from '../../../functions/meta/function-names';
import { Plus } from '../../../functions/meta/plus';
import { FUNCTION_NAMES_TEXT } from '../../../functions/text/function-names';
import { Substitute } from '../../../functions/text/substitute';
import { getObjectValue } from '../../../functions/util';
import { IFormulaCurrentConfigService } from '../../../services/current-data.service';
import { IFunctionService } from '../../../services/function.service';
import { IFormulaRuntimeService } from '../../../services/runtime.service';
import { Lexer } from '../../analysis/lexer';
import { LexerNode } from '../../analysis/lexer-node';
import { AstTreeBuilder } from '../../analysis/parser';
import { Interpreter } from '../../interpreter/interpreter';
import { generateExecuteAstNodeData } from '../../utils/ast-node-tool';
import { BaseAstNode } from '../base-ast-node';
function createWorkbookData(): IWorkbookData {
return {
id: 'test',
appVersion: '3.0.0-alpha',
locale: LocaleType.FR_FR,
name: 'Locale-independent date parsing',
sheetOrder: ['sheet1'],
sheets: {
sheet1: {
id: 'sheet1',
name: 'Sheet1',
rowCount: 1,
columnCount: 1,
},
},
styles: {},
};
}
function createDateSystemWorkbookData(id: string, dateSystem: DateSystem): IWorkbookData {
return {
...createWorkbookData(),
id,
locale: LocaleType.EN_US,
name: id,
dateSystem,
};
}
describe('FunctionNode workbook context', () => {
it('does not use the executing workbook locale to parse formula text', () => {
const testBed = createFunctionTestBed(createWorkbookData());
const currentConfigService = testBed.get(IFormulaCurrentConfigService);
const runtimeService = testBed.get(IFormulaRuntimeService);
const lexer = testBed.get(Lexer);
const astTreeBuilder = testBed.get(AstTreeBuilder);
const interpreter = testBed.get(Interpreter);
currentConfigService.load({
formulaData: {},
arrayFormulaCellData: {},
arrayFormulaRange: {},
forceCalculate: false,
dirtyRanges: [],
dirtyNameMap: {},
dirtyDefinedNameMap: {},
dirtyUnitFeatureMap: {},
dirtyUnitOtherFormulaMap: {},
excludedCell: {},
allUnitData: { [testBed.unitId]: testBed.sheetData },
});
runtimeService.setCurrent(0, 0, 1, 1, testBed.sheetId, testBed.unitId);
testBed.get(IFunctionService).registerExecutors(new Accrint(FUNCTION_NAMES_FINANCIAL.ACCRINT));
testBed.get(IFunctionService).registerExecutors(new Plus(FUNCTION_NAMES_META.PLUS));
const calculate = (formula: string) => {
const lexerNode = lexer.treeBuilder(formula);
if (!(lexerNode instanceof LexerNode)) throw new TypeError(`Failed to parse formula: ${formula}`);
const astNode = astTreeBuilder.parse(lexerNode);
if (!(astNode instanceof BaseAstNode)) throw new TypeError(`Failed to build AST: ${formula}`);
return getObjectValue(interpreter.execute(generateExecuteAstNodeData(astNode)));
};
expect(calculate('=ACCRINT("31/01/2024","31/07/2024","01/05/2024",0.1,1000,2)'))
.toBe(ErrorType.VALUE);
expect(calculate('=1+"1,5"')).toBe(ErrorType.VALUE);
});
it('uses the executing workbook date system for implicit string coercion', async () => {
const testBed = createFunctionTestBed(createDateSystemWorkbookData('date-1900', DateSystem.Date1900));
const date1904Workbook = testBed.univer.createUnit<IWorkbookData, Workbook>(
UniverInstanceType.UNIVER_SHEET,
createDateSystemWorkbookData('date-1904', DateSystem.Date1904)
);
const currentConfigService = testBed.get(IFormulaCurrentConfigService);
const runtimeService = testBed.get(IFormulaRuntimeService);
const lexer = testBed.get(Lexer);
const astTreeBuilder = testBed.get(AstTreeBuilder);
const interpreter = testBed.get(Interpreter);
currentConfigService.load({
formulaData: {},
arrayFormulaCellData: {},
arrayFormulaRange: {},
forceCalculate: false,
dirtyRanges: [],
dirtyNameMap: {},
dirtyDefinedNameMap: {},
dirtyUnitFeatureMap: {},
dirtyUnitOtherFormulaMap: {},
excludedCell: {},
allUnitData: { [testBed.unitId]: testBed.sheetData },
});
testBed.get(IFunctionService).registerExecutors(new Plus(FUNCTION_NAMES_META.PLUS));
testBed.get(IFunctionService).registerExecutors(new Abs(FUNCTION_NAMES_MATH.ABS));
testBed.get(IFunctionService).registerExecutors(new Substitute(FUNCTION_NAMES_TEXT.SUBSTITUTE));
testBed.get(IFunctionService).registerExecutors(new DateFunction(FUNCTION_NAMES_DATE.DATE));
const customDate = new CustomFunction('CUSTOMDATE');
customDate.calculateCustom = () => '1904-1-1';
const customDateArray = new CustomFunction('CUSTOMDATEARRAY');
customDateArray.calculateCustom = () => [['1904-1-1']];
const asyncCustomDate = new AsyncCustomFunction('ASYNCCUSTOMDATE');
const date1904Result = createDeferred<string>();
const date1900Result = createDeferred<string>();
const date1904Started = createDeferred<void>();
const date1900Started = createDeferred<void>();
asyncCustomDate.calculateCustom = (dateSystem) => {
if (dateSystem === '1904') {
date1904Started.resolve();
return date1904Result.promise;
}
date1900Started.resolve();
return date1900Result.promise;
};
testBed.get(IFunctionService).registerExecutors(customDate, customDateArray, asyncCustomDate);
const calculate = (unitId: string, sheetId: string, formula = '="1904-1-1"+0') => {
runtimeService.setCurrent(0, 0, 1, 1, sheetId, unitId);
const lexerNode = lexer.treeBuilder(formula);
if (!(lexerNode instanceof LexerNode)) throw new TypeError('Failed to parse implicit date coercion formula');
const astNode = astTreeBuilder.parse(lexerNode);
if (!(astNode instanceof BaseAstNode)) throw new TypeError('Failed to build implicit date coercion AST');
return getObjectValue(interpreter.execute(generateExecuteAstNodeData(astNode)));
};
const calculateAsync = async (unitId: string, sheetId: string, formula: string) => {
runtimeService.setCurrent(0, 0, 1, 1, sheetId, unitId);
const lexerNode = lexer.treeBuilder(formula);
if (!(lexerNode instanceof LexerNode)) throw new TypeError('Failed to parse async custom function formula');
const astNode = astTreeBuilder.parse(lexerNode);
if (!(astNode instanceof BaseAstNode)) throw new TypeError('Failed to build async custom function AST');
return getObjectValue(await interpreter.executeAsync(generateExecuteAstNodeData(astNode)));
};
expect(calculate(testBed.unitId, testBed.sheetId)).toBe(1462);
expect(calculate(date1904Workbook.getUnitId(), date1904Workbook.getActiveSheet()!.getSheetId())).toBe(0);
expect(calculate(testBed.unitId, testBed.sheetId, '=DATE(1904,1,1)')).toBe(1462);
expect(calculate(
date1904Workbook.getUnitId(),
date1904Workbook.getActiveSheet()!.getSheetId(),
'=DATE(1904,1,1)'
)).toBe(0);
expect(calculate(
date1904Workbook.getUnitId(),
date1904Workbook.getActiveSheet()!.getSheetId(),
'=ABS("1904-1-1")'
)).toBe(0);
expect(calculate(
date1904Workbook.getUnitId(),
date1904Workbook.getActiveSheet()!.getSheetId(),
'=ABS(SUBSTITUTE("1904/1/1","/","-"))'
)).toBe(0);
expect(calculate(testBed.unitId, testBed.sheetId, '=ABS(SUBSTITUTE("1904/1/1","/","-"))')).toBe(1462);
expect(calculate(
date1904Workbook.getUnitId(),
date1904Workbook.getActiveSheet()!.getSheetId(),
'=ABS(CUSTOMDATE())'
)).toBe(0);
expect(calculate(
date1904Workbook.getUnitId(),
date1904Workbook.getActiveSheet()!.getSheetId(),
'=ABS(CUSTOMDATEARRAY())'
)).toStrictEqual([[0]]);
runtimeService.setCurrent(
0,
0,
1,
1,
date1904Workbook.getActiveSheet()!.getSheetId(),
date1904Workbook.getUnitId()
);
const lambdaLexerNode = lexer.treeBuilder('=LAMBDA(value,ABS(value))');
if (!(lambdaLexerNode instanceof LexerNode)) throw new TypeError('Failed to parse lambda formula');
const lambdaAstNode = astTreeBuilder.parse(lambdaLexerNode);
if (!(lambdaAstNode instanceof BaseAstNode)) throw new TypeError('Failed to build lambda AST');
const lambda = interpreter.execute(generateExecuteAstNodeData(lambdaAstNode)) as LambdaValueObjectObject;
expect(lambda.executeCustom('1904-1-1').getValue()).toBe(0);
const pending1904 = calculateAsync(
date1904Workbook.getUnitId(),
date1904Workbook.getActiveSheet()!.getSheetId(),
'=ABS(ASYNCCUSTOMDATE("1904"))'
);
await date1904Started.promise;
const pending1900 = calculateAsync(testBed.unitId, testBed.sheetId, '=ABS(ASYNCCUSTOMDATE("1900"))');
await date1900Started.promise;
date1904Result.resolve('1904-1-1');
expect(await pending1904).toBe(0);
date1900Result.resolve('1904-1-1');
expect(await pending1900).toBe(1462);
expect(calculate(testBed.unitId, testBed.sheetId)).toBe(1462);
});
});
function createDeferred<T>() {
let resolve!: (value: T) => void;
const promise = new Promise<T>((promiseResolve) => {
resolve = promiseResolve;
});
return { promise, resolve };
}
@@ -14,11 +14,9 @@
* limitations under the License.
*/
import type { Nullable } from '@univerjs/core';
import type { DateSystem, Nullable } from '@univerjs/core';
import type { ErrorType } from '../../basics/error-type';
import type { FunctionVariantType } from '../reference-object/base-reference-object';
import { AstNodePromiseType } from '../../basics/common';
import { ErrorValueObject } from '../value-object/base-value-object';
import { NodeType } from './node-type';
@@ -144,7 +142,7 @@ export class BaseAstNode {
this._calculateState = true;
}
execute() {
execute(_dateSystem?: DateSystem) {
/* abstract */
}
@@ -152,7 +150,7 @@ export class BaseAstNode {
/* abstract */
}
async executeAsync(): Promise<AstNodePromiseType> {
async executeAsync(_dateSystem?: DateSystem): Promise<AstNodePromiseType> {
/* abstract */
return Promise.resolve(AstNodePromiseType.SUCCESS);
}
@@ -15,7 +15,6 @@
*/
import type { BaseFunction } from '../../functions/base-function';
import type { LexerNode } from '../analysis/lexer-node';
import type {
AsyncArrayObject,
@@ -26,7 +25,7 @@ import type {
} from '../reference-object/base-reference-object';
import type { BaseValueObject } from '../value-object/base-value-object';
import type { FormulaFunctionResultValueType } from '../value-object/primitive-object';
import { Inject, Injector } from '@univerjs/core';
import { DateSystem, Inject, Injector } from '@univerjs/core';
import { AstNodePromiseType } from '../../basics/common';
import { ErrorType } from '../../basics/error-type';
import { matchToken } from '../../basics/token';
@@ -90,14 +89,14 @@ export class FunctionNode extends BaseAstNode {
return this._functionExecutor.lazyIfReferenceArrayArgumentIndexes.includes(this.getChildren().indexOf(child));
}
override async executeAsync() {
override async executeAsync(dateSystem: DateSystem = DateSystem.Date1900) {
const children = this.getChildren();
this._compatibility();
const resultVariant = this._functionExecutor.needsAstChildren
? this._calculateAst(children)
: await this._calculateAsync(this._collectVariants(children));
? this._calculateAst(children, dateSystem)
: await this._calculateAsync(this._collectVariants(children, dateSystem), dateSystem);
let result: FunctionVariantType;
if (resultVariant.isAsyncObject() || resultVariant.isAsyncArrayObject()) {
@@ -106,7 +105,7 @@ export class FunctionNode extends BaseAstNode {
result = resultVariant as FunctionVariantType;
}
result = this._normalizeTopLevelLegacyArrayResult(result);
result = this._normalizeTopLevelLegacyArrayResult(result).withDateSystem(dateSystem) as FunctionVariantType;
this._setEmbeddedArrayFormulaToResult(result);
@@ -117,16 +116,16 @@ export class FunctionNode extends BaseAstNode {
return Promise.resolve(AstNodePromiseType.SUCCESS);
}
override execute() {
override execute(dateSystem: DateSystem = DateSystem.Date1900) {
const children = this.getChildren();
this._compatibility();
const resultVariant = (this._functionExecutor.needsAstChildren
? this._calculateAst(children)
: this._calculate(this._collectVariants(children))) as FunctionVariantType;
? this._calculateAst(children, dateSystem)
: this._calculate(this._collectVariants(children, dateSystem), dateSystem)) as FunctionVariantType;
const result = this._normalizeTopLevelLegacyArrayResult(resultVariant);
const result = this._normalizeTopLevelLegacyArrayResult(resultVariant).withDateSystem(dateSystem) as FunctionVariantType;
this._setEmbeddedArrayFormulaToResult(result);
@@ -135,12 +134,12 @@ export class FunctionNode extends BaseAstNode {
this.setValue(result as FunctionVariantType);
}
private _collectVariants(children: BaseAstNode[]): BaseValueObject[] {
private _collectVariants(children: BaseAstNode[], dateSystem: DateSystem): BaseValueObject[] {
const variants: BaseValueObject[] = [];
const childrenCount = children.length;
for (let i = 0; i < childrenCount; i++) {
const object = this._getChildVariant(children[i]);
const object = this._getChildVariant(children[i], dateSystem);
if (object == null) {
continue;
@@ -152,7 +151,7 @@ export class FunctionNode extends BaseAstNode {
return variants;
}
private _getChildVariant(child: BaseAstNode): FunctionVariantType | undefined {
private _getChildVariant(child: BaseAstNode, dateSystem: DateSystem): FunctionVariantType | undefined {
const object = child.getValue();
if (object == null) {
@@ -166,10 +165,10 @@ export class FunctionNode extends BaseAstNode {
// In the SUBTOTAL function, we need to get rowData information, we can only use ReferenceObject
if (object.isReferenceObject() && !this._functionExecutor.needsReferenceObject) {
// Array converted from reference object needs to be marked
return (object as BaseReferenceObject).toArrayValueObject();
return (object as BaseReferenceObject).withDateSystem(dateSystem).toArrayValueObject();
}
return object;
return object.withDateSystem(dateSystem);
}
isFunctionExecutorArgumentsIgnoreNumberPattern() {
@@ -279,12 +278,15 @@ export class FunctionNode extends BaseAstNode {
/**
* Transform the result of a custom function to a NodeValueType.
*/
private _handleCustomResult(resultVariantCustom: FormulaFunctionResultValueType): NodeValueType {
private _handleCustomResult(
resultVariantCustom: FormulaFunctionResultValueType,
dateSystem: DateSystem
): NodeValueType {
if (typeof resultVariantCustom !== 'object' || resultVariantCustom == null) {
return ValueObjectFactory.create(resultVariantCustom);
return ValueObjectFactory.create(resultVariantCustom, false, dateSystem);
}
const arrayValues = transformToValueObject(resultVariantCustom);
const arrayValues = transformToValueObject(resultVariantCustom, false, dateSystem);
return ArrayValueObject.create({
calculateValueList: arrayValues,
rowCount: arrayValues.length,
@@ -322,7 +324,7 @@ export class FunctionNode extends BaseAstNode {
});
}
private _calculate(variants: BaseValueObject[]) {
private _calculate(variants: BaseValueObject[], dateSystem: DateSystem) {
// Check the number of parameters
const { minParams, maxParams } = this._functionExecutor;
if (minParams !== -1 && maxParams !== -1 && (variants.length < minParams || variants.length > maxParams)) {
@@ -337,7 +339,7 @@ export class FunctionNode extends BaseAstNode {
...this._mapVariantsToValues(variants)
) as FormulaFunctionResultValueType;
resultVariant = this._handleCustomResult(resultVariantCustom);
resultVariant = this._handleCustomResult(resultVariantCustom, dateSystem);
} else {
this._handleAddressFunction();
resultVariant = this._functionExecutor.calculate(...variants);
@@ -346,7 +348,7 @@ export class FunctionNode extends BaseAstNode {
return resultVariant;
}
private _calculateAst(children: BaseAstNode[]) {
private _calculateAst(children: BaseAstNode[], dateSystem: DateSystem) {
const { minParams, maxParams } = this._functionExecutor;
if (minParams !== -1 && maxParams !== -1 && (children.length < minParams || children.length > maxParams)) {
return ErrorValueObject.create(ErrorType.NA);
@@ -355,10 +357,10 @@ export class FunctionNode extends BaseAstNode {
this._setRefInfo();
this._handleAddressFunction();
return this._functionExecutor.calculateAst(children, (node) => this._getChildVariant(node) ?? null);
return this._functionExecutor.calculateAst(children, (node) => this._getChildVariant(node, dateSystem) ?? null);
}
private async _calculateAsync(variants: BaseValueObject[]) {
private async _calculateAsync(variants: BaseValueObject[], dateSystem: DateSystem) {
// Check the number of parameters
const { minParams, maxParams } = this._functionExecutor;
if (minParams !== -1 && maxParams !== -1 && (variants.length < minParams || variants.length > maxParams)) {
@@ -374,7 +376,7 @@ export class FunctionNode extends BaseAstNode {
...this._mapVariantsToValues(variants)
);
resultVariant = this._handleCustomResult(resultVariantCustom);
resultVariant = this._handleCustomResult(resultVariantCustom, dateSystem);
} else {
this._handleAddressFunction();
resultVariant = this._functionExecutor.calculate(...variants);
@@ -400,6 +402,7 @@ export class FunctionNode extends BaseAstNode {
const { currentUnitId, currentSubUnitId, currentRow, currentColumn, currentRowCount, currentColumnCount } = this._runtimeService;
this._functionExecutor.setRefInfo(currentUnitId, currentSubUnitId, currentRow, currentColumn, currentRowCount, currentColumnCount);
this._functionExecutor.setDateSystem(this._currentConfigService.getDateSystem(currentUnitId));
if (this._functionExecutor.needsSheetRowColumnCount) {
const { rowCount, columnCount } = this._currentConfigService.getSheetRowColumnCount(currentUnitId, currentSubUnitId);
@@ -15,12 +15,12 @@
*/
import type { compareToken } from '../../basics/token';
import type { BaseFunction } from '../../functions/base-function';
import type { Compare } from '../../functions/meta/compare';
import type { BaseReferenceObject, FunctionVariantType } from '../reference-object/base-reference-object';
import type { ArrayValueObject } from '../value-object/array-value-object';
import type { BaseValueObject } from '../value-object/base-value-object';
import { DateSystem } from '@univerjs/core';
import { ErrorType } from '../../basics/error-type';
import { OPERATOR_TOKEN_COMPARE_SET, OPERATOR_TOKEN_SET, operatorToken } from '../../basics/token';
import { FUNCTION_NAMES_MATH } from '../../functions/math/function-names';
@@ -48,7 +48,7 @@ export class OperatorNode extends BaseAstNode {
return NodeType.OPERATOR;
}
override execute() {
override execute(dateSystem: DateSystem = DateSystem.Date1900) {
const children = this.getChildren();
if (this._functionExecutor.name === FUNCTION_NAMES_META.COMPARE) {
(this._functionExecutor as Compare).setCompareType(this.getToken() as compareToken);
@@ -83,6 +83,8 @@ export class OperatorNode extends BaseAstNode {
object2 = this._referenceToOperatorValue(object2 as BaseReferenceObject);
}
object1 = object1.withDateSystem(dateSystem);
object2 = object2.withDateSystem(dateSystem);
const result = this._functionExecutor.calculate(
object1 as BaseValueObject,
object2 as BaseValueObject
@@ -90,7 +92,7 @@ export class OperatorNode extends BaseAstNode {
this._setEmbeddedArrayFormulaToResult(result);
this.setValue(result);
this.setValue(result.withDateSystem(dateSystem));
}
private _referenceToOperatorValue(reference: BaseReferenceObject): BaseValueObject {
@@ -16,9 +16,9 @@
import type { Nullable } from '@univerjs/core';
import type { BaseFunction } from '../../functions/base-function';
import type { BaseReferenceObject, FunctionVariantType } from '../reference-object/base-reference-object';
import type { BaseValueObject } from '../value-object/base-value-object';
import { DateSystem } from '@univerjs/core';
import { ErrorType } from '../../basics/error-type';
import { OPERATOR_TOKEN_COMPARE_SET, prefixToken } from '../../basics/token';
import { FUNCTION_NAMES_LOOKUP } from '../../functions/lookup/function-names';
@@ -46,7 +46,7 @@ export class PrefixNode extends BaseAstNode {
return NodeType.PREFIX;
}
override execute() {
override execute(dateSystem: DateSystem = DateSystem.Date1900) {
const children = this.getChildren();
let value = children[0].getValue();
let result: FunctionVariantType;
@@ -58,18 +58,18 @@ export class PrefixNode extends BaseAstNode {
if (this._operatorString === prefixToken.MINUS) {
if (value.isReferenceObject()) {
value = (value as BaseReferenceObject).toArrayValueObject();
value = (value as BaseReferenceObject).withDateSystem(dateSystem).toArrayValueObject();
}
result = this._functionExecutor!.calculate(
NumberValueObject.create(0),
value as BaseValueObject
(value as BaseValueObject).withDateSystem(dateSystem)
) as FunctionVariantType;
} else if (this._operatorString === prefixToken.AT) {
result = this._handlerAT(value);
} else {
result = ErrorValueObject.create(ErrorType.VALUE);
}
this.setValue(result);
this.setValue(result.withDateSystem(dateSystem));
}
private _handlerAT(value: FunctionVariantType) {
@@ -15,11 +15,10 @@
*/
import type { BaseFunction } from '../../functions/base-function';
import type { BaseReferenceObject, FunctionVariantType } from '../reference-object/base-reference-object';
import type { CellReferenceObject } from '../reference-object/cell-reference-object';
import type { BaseValueObject } from '../value-object/base-value-object';
import { Inject } from '@univerjs/core';
import { DateSystem, Inject } from '@univerjs/core';
import { ErrorType } from '../../basics/error-type';
import { suffixToken } from '../../basics/token';
import { FUNCTION_NAMES_META } from '../../functions/meta/function-names';
@@ -47,7 +46,7 @@ export class SuffixNode extends BaseAstNode {
return NodeType.SUFFIX;
}
override execute() {
override execute(dateSystem: DateSystem = DateSystem.Date1900) {
const children = this.getChildren();
let value = children[0]?.getValue();
let result: FunctionVariantType;
@@ -58,10 +57,10 @@ export class SuffixNode extends BaseAstNode {
if (this._operatorString === suffixToken.PERCENTAGE) {
if (value.isReferenceObject()) {
value = (value as BaseReferenceObject).toArrayValueObject();
value = (value as BaseReferenceObject).withDateSystem(dateSystem).toArrayValueObject();
}
result = this._functionExecutor!.calculate(
value as BaseValueObject,
(value as BaseValueObject).withDateSystem(dateSystem),
NumberValueObject.create(100)
) as FunctionVariantType;
@@ -75,7 +74,7 @@ export class SuffixNode extends BaseAstNode {
} else {
result = ErrorValueObject.create(ErrorType.VALUE);
}
this.setValue(result);
this.setValue(result.withDateSystem(dateSystem));
}
private _handlerPound(value: FunctionVariantType) {
@@ -16,6 +16,7 @@
import type { ErrorType } from '../../basics/error-type';
import type { FunctionNode } from './';
import { DateSystem } from '@univerjs/core';
import { BooleanValue } from '../../basics/common';
import { ERROR_TYPE_SET } from '../../basics/error-type';
import { LexerNode } from '../analysis/lexer-node';
@@ -34,11 +35,13 @@ export class ValueNode extends BaseAstNode {
return NodeType.VALUE;
}
override execute(): void {
override execute(dateSystem: DateSystem = DateSystem.Date1900): void {
const token = this.getToken();
const tokenTrim = token.trim();
if (tokenTrim.startsWith('"') && tokenTrim.endsWith('"')) {
this.setValue(StringValueObject.create(tokenTrim.slice(1, -1).replace(/""/g, '"')));
this.setValue(
StringValueObject.create(tokenTrim.slice(1, -1).replace(/""/g, '"')).withDateSystem(dateSystem)
);
return;
}
@@ -47,7 +50,7 @@ export class ValueNode extends BaseAstNode {
if (parent?.nodeType === NodeType.FUNCTION) {
isIgnoreNumberPattern = (parent as FunctionNode).isFunctionExecutorArgumentsIgnoreNumberPattern?.() ?? true;
}
this.setValue(ValueObjectFactory.create(token, isIgnoreNumberPattern));
this.setValue(ValueObjectFactory.create(token, isIgnoreNumberPattern, dateSystem));
}
}
@@ -16,6 +16,7 @@
import { describe, expect, it, vi } from 'vitest';
import { AstNodePromiseType } from '../../../basics/common';
import { createFunctionTestBed } from '../../../functions/__tests__/create-function-test-bed';
import { BaseAstNode } from '../../ast-node/base-ast-node';
import { NumberValueObject } from '../../value-object/primitive-object';
import { Interpreter } from '../interpreter';
@@ -38,11 +39,8 @@ class AsyncReferenceLikeNode extends BaseAstNode {
describe('Interpreter async nodes', () => {
it('detects and awaits an async root node regardless of node type', async () => {
const interpreter = new Interpreter({
currentColumn: 0,
currentRow: 0,
isStopExecution: () => false,
});
const testBed = createFunctionTestBed();
const interpreter = testBed.get(Interpreter);
const node = new AsyncReferenceLikeNode();
expect(interpreter.checkAsyncNode(node)).toBe(true);
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import type { Nullable } from '@univerjs/core';
import type { DateSystem, Nullable } from '@univerjs/core';
import type { BaseAstNode } from '../ast-node/base-ast-node';
import type { LambdaNode } from '../ast-node/lambda-node';
import type { ReferenceNode } from '../ast-node/reference-node';
@@ -27,6 +27,7 @@ import { Disposable } from '@univerjs/core';
import { AstNodePromiseType } from '../../basics/common';
import { ErrorType } from '../../basics/error-type';
import { DEFAULT_TOKEN_LAMBDA_FUNCTION_NAME } from '../../basics/token-type';
import { IFormulaCurrentConfigService } from '../../services/current-data.service';
import { IFormulaRuntimeService } from '../../services/runtime.service';
import { NodeType } from '../ast-node/node-type';
import { ErrorValueObject } from '../value-object/base-value-object';
@@ -34,11 +35,14 @@ import { BooleanValueObject, NumberValueObject } from '../value-object/primitive
type InterpreterRuntimeService = Pick<
IFormulaRuntimeService,
'currentColumn' | 'currentRow' | 'isStopExecution'
'currentColumn' | 'currentRow' | 'currentUnitId' | 'isStopExecution'
>;
export class Interpreter extends Disposable {
constructor(@IFormulaRuntimeService private readonly _runtimeService: InterpreterRuntimeService) {
constructor(
@IFormulaRuntimeService private readonly _runtimeService: InterpreterRuntimeService,
@IFormulaCurrentConfigService private readonly _currentConfigService: IFormulaCurrentConfigService
) {
super();
}
@@ -55,7 +59,7 @@ export class Interpreter extends Disposable {
const refOffsetX = nodeData.refOffsetX;
const refOffsetY = nodeData.refOffsetY;
await this._executeAsync(node, refOffsetX, refOffsetY);
await this._executeAsync(node, this.getDateSystem(), refOffsetX, refOffsetY);
const value = node.getValue();
@@ -79,7 +83,7 @@ export class Interpreter extends Disposable {
const refOffsetX = nodeData.refOffsetX;
const refOffsetY = nodeData.refOffsetY;
this._execute(node, refOffsetX, refOffsetY);
this._execute(node, this.getDateSystem(), refOffsetX, refOffsetY);
const value = node.getValue();
@@ -91,7 +95,7 @@ export class Interpreter extends Disposable {
}
executePreCalculateNode(node: PreCalculateNodeType) {
node.execute();
node.execute(this.getDateSystem());
return node.getValue();
}
@@ -125,11 +129,16 @@ export class Interpreter extends Disposable {
}
}
private async _executeAsync(node: BaseAstNode, refOffsetX = 0, refOffsetY = 0): Promise<AstNodePromiseType> {
private async _executeAsync(
node: BaseAstNode,
dateSystem: DateSystem,
refOffsetX = 0,
refOffsetY = 0
): Promise<AstNodePromiseType> {
if (this._runtimeService.isStopExecution()) {
return Promise.resolve(AstNodePromiseType.ERROR);
}
if (await this._executeLazyFunctionAsync(node, refOffsetX, refOffsetY)) {
if (await this._executeLazyFunctionAsync(node, dateSystem, refOffsetX, refOffsetY)) {
return Promise.resolve(AstNodePromiseType.SUCCESS);
}
const children = node.getChildren();
@@ -144,10 +153,10 @@ export class Interpreter extends Disposable {
token.toUpperCase() === DEFAULT_TOKEN_LAMBDA_FUNCTION_NAME &&
(item as LambdaNode).isEmptyParamFunction()
) {
item.execute();
item.execute(dateSystem);
continue;
}
await this._executeAsync(item, refOffsetX, refOffsetY);
await this._executeAsync(item, dateSystem, refOffsetX, refOffsetY);
}
if (node.nodeType === NodeType.REFERENCE) {
@@ -155,19 +164,24 @@ export class Interpreter extends Disposable {
}
if (node.isAsync()) {
await node.executeAsync();
await node.executeAsync(dateSystem);
} else {
node.execute();
node.execute(dateSystem);
}
return Promise.resolve(AstNodePromiseType.SUCCESS);
}
private _execute(node: BaseAstNode, refOffsetX = 0, refOffsetY = 0): AstNodePromiseType {
private _execute(
node: BaseAstNode,
dateSystem: DateSystem,
refOffsetX = 0,
refOffsetY = 0
): AstNodePromiseType {
if (this._runtimeService.isStopExecution()) {
return AstNodePromiseType.ERROR;
}
if (this._executeLazyFunction(node, refOffsetX, refOffsetY)) {
if (this._executeLazyFunction(node, dateSystem, refOffsetX, refOffsetY)) {
return AstNodePromiseType.SUCCESS;
}
const children = node.getChildren();
@@ -182,58 +196,73 @@ export class Interpreter extends Disposable {
token.toUpperCase() === DEFAULT_TOKEN_LAMBDA_FUNCTION_NAME &&
(item as LambdaNode).isEmptyParamFunction()
) {
item.execute();
item.execute(dateSystem);
continue;
}
this._execute(item, refOffsetX, refOffsetY);
this._execute(item, dateSystem, refOffsetX, refOffsetY);
}
if (node.nodeType === NodeType.REFERENCE) {
(node as ReferenceNode).setRefOffset(refOffsetX, refOffsetY);
}
node.execute();
node.execute(dateSystem);
return AstNodePromiseType.SUCCESS;
}
private async _executeLazyFunctionAsync(node: BaseAstNode, refOffsetX: number, refOffsetY: number): Promise<boolean> {
private async _executeLazyFunctionAsync(
node: BaseAstNode,
dateSystem: DateSystem,
refOffsetX: number,
refOffsetY: number
): Promise<boolean> {
if (node.nodeType !== NodeType.FUNCTION) {
return false;
}
const token = node.getToken().toUpperCase();
if (token === 'IF') {
return await this._executeLazyIfAsync(node, refOffsetX, refOffsetY);
return await this._executeLazyIfAsync(node, dateSystem, refOffsetX, refOffsetY);
}
if (token === 'IFERROR') {
return await this._executeLazyIfErrorAsync(node, refOffsetX, refOffsetY);
return await this._executeLazyIfErrorAsync(node, dateSystem, refOffsetX, refOffsetY);
}
return false;
}
private _executeLazyFunction(node: BaseAstNode, refOffsetX: number, refOffsetY: number): boolean {
private _executeLazyFunction(
node: BaseAstNode,
dateSystem: DateSystem,
refOffsetX: number,
refOffsetY: number
): boolean {
if (node.nodeType !== NodeType.FUNCTION) {
return false;
}
const token = node.getToken().toUpperCase();
if (token === 'IF') {
return this._executeLazyIf(node, refOffsetX, refOffsetY);
return this._executeLazyIf(node, dateSystem, refOffsetX, refOffsetY);
}
if (token === 'IFERROR') {
return this._executeLazyIfError(node, refOffsetX, refOffsetY);
return this._executeLazyIfError(node, dateSystem, refOffsetX, refOffsetY);
}
return false;
}
private async _executeLazyIfAsync(node: BaseAstNode, refOffsetX: number, refOffsetY: number): Promise<boolean> {
private async _executeLazyIfAsync(
node: BaseAstNode,
dateSystem: DateSystem,
refOffsetX: number,
refOffsetY: number
): Promise<boolean> {
const children = node.getChildren();
if (children.length < 2 || children.length > 3) {
return false;
}
await this._executeAsync(children[0], refOffsetX, refOffsetY);
await this._executeAsync(children[0], dateSystem, refOffsetX, refOffsetY);
const condition = children[0].getValue();
if (condition == null || condition.isReferenceObject() || condition.isArray()) {
return false;
@@ -249,18 +278,23 @@ export class Interpreter extends Disposable {
return true;
}
await this._executeAsync(selected, refOffsetX, refOffsetY);
await this._executeAsync(selected, dateSystem, refOffsetX, refOffsetY);
node.setValue(this._toLazyIfSelectedValue(selected.getValue(), node));
return true;
}
private _executeLazyIf(node: BaseAstNode, refOffsetX: number, refOffsetY: number): boolean {
private _executeLazyIf(
node: BaseAstNode,
dateSystem: DateSystem,
refOffsetX: number,
refOffsetY: number
): boolean {
const children = node.getChildren();
if (children.length < 2 || children.length > 3) {
return false;
}
this._execute(children[0], refOffsetX, refOffsetY);
this._execute(children[0], dateSystem, refOffsetX, refOffsetY);
const condition = children[0].getValue();
if (condition == null || condition.isReferenceObject() || condition.isArray()) {
return false;
@@ -276,18 +310,23 @@ export class Interpreter extends Disposable {
return true;
}
this._execute(selected, refOffsetX, refOffsetY);
this._execute(selected, dateSystem, refOffsetX, refOffsetY);
node.setValue(this._toLazyIfSelectedValue(selected.getValue(), node));
return true;
}
private async _executeLazyIfErrorAsync(node: BaseAstNode, refOffsetX: number, refOffsetY: number): Promise<boolean> {
private async _executeLazyIfErrorAsync(
node: BaseAstNode,
dateSystem: DateSystem,
refOffsetX: number,
refOffsetY: number
): Promise<boolean> {
const children = node.getChildren();
if (children.length !== 2) {
return false;
}
await this._executeAsync(children[0], refOffsetX, refOffsetY);
await this._executeAsync(children[0], dateSystem, refOffsetX, refOffsetY);
const rawValue = children[0].getValue();
const value = this._toLazyIfErrorValue(rawValue);
if (value == null || value.isArray()) {
@@ -298,18 +337,23 @@ export class Interpreter extends Disposable {
return true;
}
await this._executeAsync(children[1], refOffsetX, refOffsetY);
await this._executeAsync(children[1], dateSystem, refOffsetX, refOffsetY);
node.setValue(children[1].getValue() ?? ErrorValueObject.create(ErrorType.VALUE));
return true;
}
private _executeLazyIfError(node: BaseAstNode, refOffsetX: number, refOffsetY: number): boolean {
private _executeLazyIfError(
node: BaseAstNode,
dateSystem: DateSystem,
refOffsetX: number,
refOffsetY: number
): boolean {
const children = node.getChildren();
if (children.length !== 2) {
return false;
}
this._execute(children[0], refOffsetX, refOffsetY);
this._execute(children[0], dateSystem, refOffsetX, refOffsetY);
const rawValue = children[0].getValue();
const value = this._toLazyIfErrorValue(rawValue);
if (value == null || value.isArray()) {
@@ -320,11 +364,15 @@ export class Interpreter extends Disposable {
return true;
}
this._execute(children[1], refOffsetX, refOffsetY);
this._execute(children[1], dateSystem, refOffsetX, refOffsetY);
node.setValue(children[1].getValue() ?? ErrorValueObject.create(ErrorType.VALUE));
return true;
}
getDateSystem(): DateSystem {
return this._currentConfigService.getDateSystem(this._runtimeService.currentUnitId);
}
private _toLazyIfErrorValue(value: Nullable<FunctionVariantType>): Nullable<BaseValueObject> {
if (value == null) {
return null;
@@ -17,7 +17,7 @@
import type { ICellData, IRange, IUnitRange, Nullable } from '@univerjs/core';
import type { IArrayFormulaRangeType, IRuntimeUnitDataType, IUnitData, IUnitSheetNameMap, IUnitStylesData } from '../../basics/common';
import type { BaseValueObject, IArrayValueObject } from '../value-object/base-value-object';
import { CellValueType, isNullCell, isTextFormat, moveRangeByOffset, ObjectMatrix } from '@univerjs/core';
import { CellValueType, DateSystem, isNullCell, isTextFormat, moveRangeByOffset, ObjectMatrix } from '@univerjs/core';
import { FormulaAstLRU } from '../../basics/cache-lru';
import { ERROR_TYPE_SET, ErrorType } from '../../basics/error-type';
import { ObjectClassType } from '../../basics/object-class-type';
@@ -41,6 +41,8 @@ const FORMULA_CACHE_LRU_COUNT = 10000;
export const FORMULA_REF_TO_ARRAY_CACHE = new FormulaAstLRU<ArrayValueObject>(FORMULA_CACHE_LRU_COUNT);
export class BaseReferenceObject extends ObjectClassType {
private _dateSystem = DateSystem.Date1900;
private _forcedSheetId: Nullable<string> = '';
private _forcedSheetName: string = '';
@@ -154,6 +156,15 @@ export class BaseReferenceObject extends ObjectClassType {
return true;
}
getDateSystem() {
return this._dateSystem;
}
withDateSystem(dateSystem: DateSystem): this {
this._dateSystem = dateSystem;
return this;
}
iterator(
callback: (valueObject: Nullable<BaseValueObject>, rowIndex: number, columnIndex: number) => Nullable<boolean>
) {
@@ -187,7 +198,7 @@ export class BaseReferenceObject extends ObjectClassType {
const pattern = this.getCellPattern(unitId, sheetId, r, c);
if (pattern && resultObjectValue.isNumber()) {
const value = Number(resultObjectValue.getValue());
resultObjectValue = NumberValueObject.create(value, pattern);
resultObjectValue = NumberValueObject.create(value, pattern).withDateSystem(this._dateSystem);
}
}
@@ -221,7 +232,7 @@ export class BaseReferenceObject extends ObjectClassType {
const pattern = this.getCellPattern(unitId, sheetId, startRow, startColumn);
if (pattern && cellValueObject.isNumber()) {
const value = Number(cellValueObject.getValue());
cellValueObject = NumberValueObject.create(value, pattern);
cellValueObject = NumberValueObject.create(value, pattern).withDateSystem(this._dateSystem);
}
return cellValueObject;
@@ -464,20 +475,20 @@ export class BaseReferenceObject extends ObjectClassType {
const pattern = this._getPatternByCell(cell);
if (isTextFormat(pattern)) {
return StringValueObject.create(value.toString());
return StringValueObject.create(value.toString()).withDateSystem(this._dateSystem);
}
return createNumberValueObjectByRawValue(value, pattern);
return createNumberValueObjectByRawValue(value, pattern).withDateSystem(this._dateSystem);
}
if (cellValueType === CellValueType.STRING || cellValueType === CellValueType.FORCE_STRING) {
// A1 is `"test"`, =A1 also needs to get `"test"`
return StringValueObject.create(value.toString());
return StringValueObject.create(value.toString()).withDateSystem(this._dateSystem);
}
if (cellValueType === CellValueType.BOOLEAN) {
return createBooleanValueObjectByRawValue(value);
}
return ValueObjectFactory.create(value);
return ValueObjectFactory.create(value, false, this._dateSystem);
}
private _getPatternByCell(cell: ICellData) {
@@ -629,7 +640,8 @@ export class BaseReferenceObject extends ObjectClassType {
toArrayValueObject(useCache: boolean = true): ArrayValueObject {
const { startRow, endRow, startColumn, endColumn } = this.getRangePosition();
const key = `${this.getUnitId()}_${this.getSheetId()}_${startRow}_${endRow}_${startColumn}_${endColumn}`;
// A cached range can be reused only when its string and formatted-number interpretation matches.
const key = `${this.getUnitId()}_${this.getSheetId()}_${startRow}_${endRow}_${startColumn}_${endColumn}_${this._dateSystem}`;
const array = FORMULA_REF_TO_ARRAY_CACHE.get(key);
@@ -670,7 +682,7 @@ export class BaseReferenceObject extends ObjectClassType {
useInvertedIndexCache: true,
};
const arrayValueObject = ArrayValueObject.create(arrayValueObjectData);
const arrayValueObject = ArrayValueObject.create(arrayValueObjectData, this._dateSystem);
useCache && FORMULA_REF_TO_ARRAY_CACHE.set(key, arrayValueObject);
@@ -707,7 +719,7 @@ export class BaseReferenceObject extends ObjectClassType {
column: 0,
};
return ArrayValueObject.create(arrayValueObjectData);
return ArrayValueObject.create(arrayValueObjectData, this._dateSystem);
}
}
@@ -14,9 +14,15 @@
* limitations under the License.
*/
import { DateSystem } from '@univerjs/core';
import { describe, expect, it } from 'vitest';
import { operatorToken } from '../../../basics/token';
import { compareNumfmtPriority, comparePatternPriority } from '../numfmt-kit';
import {
clearStringToNumberPatternCache,
compareNumfmtPriority,
comparePatternPriority,
stringIsNumberPattern,
} from '../numfmt-kit';
const numfmtMap = {
currency: '"¥"#,##0.00_);[Red]("¥"#,##0.00)',
@@ -184,4 +190,14 @@ describe('Test numfmt kit', () => {
expect(comparePatternPriority(numfmtMap.date, '', operatorToken.PLUS)).toBe(numfmtMap.date);
expect(comparePatternPriority('', numfmtMap.accounting, operatorToken.MINUS)).toBe(numfmtMap.accounting);
});
it('isolates parsed string-number patterns by date system', () => {
clearStringToNumberPatternCache();
expect(stringIsNumberPattern('1904-1-1', {
dateSystem: DateSystem.Date1900,
})).toMatchObject({ isNumberPattern: true, value: 1462 });
expect(stringIsNumberPattern('1904-1-1', {
dateSystem: DateSystem.Date1904,
})).toMatchObject({ isNumberPattern: true, value: 0 });
});
});
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { cellToRange, CellValueType } from '@univerjs/core';
import { cellToRange, CellValueType, DateSystem } from '@univerjs/core';
import { describe, expect, it } from 'vitest';
import { ErrorType } from '../../../basics/error-type';
import { compareToken } from '../../../basics/token';
@@ -25,7 +25,8 @@ import { RowReferenceObject } from '../../reference-object/row-reference-object'
import { ArrayValueObject } from '../../value-object/array-value-object';
import { ErrorValueObject } from '../../value-object/base-value-object';
import { BooleanValueObject, NullValueObject, NumberValueObject, StringValueObject } from '../../value-object/primitive-object';
import { convertTonNumber, isSingleValueObject, objectValueToCellValue } from '../value-object';
import { valueObjectCompare } from '../object-compare';
import { convertTonNumber, filterSameValueObjectResult, isSingleValueObject, objectValueToCellValue } from '../value-object';
describe('Test object cover', () => {
it('Function convertTonNumber', () => {
@@ -38,6 +39,17 @@ describe('Test object cover', () => {
expect(BooleanValueObject.create(false).compare(StringValueObject.create(''), compareToken.NOT_EQUAL).getValue()).toBe(true);
});
it('uses the supplied date system when matching date criteria against serial numbers', () => {
const range = ArrayValueObject.createByArray([[0]]);
const criteria = StringValueObject.create('1904-1-1').withDateSystem(DateSystem.Date1904);
const comparison = valueObjectCompare(range, criteria) as ArrayValueObject;
expect(comparison.get(0, 0)?.getValue()).toBe(true);
const result = filterSameValueObjectResult(comparison, range, criteria);
expect(result.get(0, 0)?.getValue()).toBe(true);
});
it('Function isSingleValueObject', () => {
expect(isSingleValueObject(BooleanValueObject.create(true))).toBe(true);
expect(isSingleValueObject(BooleanValueObject.create(false))).toBe(true);
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import type { ICellData, Nullable, Styles } from '@univerjs/core';
import type { DateSystem, ICellData, Nullable, Styles } from '@univerjs/core';
import { currencySymbols, getNumfmtParseValueFilter, LocaleType, numfmt } from '@univerjs/core';
import { FormulaAstLRU } from '../../basics/cache-lru';
import { operatorToken } from '../../basics/token';
@@ -128,7 +128,7 @@ export function handleNumfmtInCell(oldCell: Nullable<ICellData>, cell: Nullable<
*
* @param oldPattern
* @param pattern
* @returns
* @returns The parsed number and format pattern when the input is recognized.
*/
export function compareNumfmtPriority(oldPattern: string, pattern: string) {
const oldPatternType = getNumberFormatType(oldPattern);
@@ -306,7 +306,8 @@ export function applyCurrencyFormat(locale: LocaleType, number: number, numberDi
* "2012-12-12"
* "16:48:00"
*
* @param locale
* @param input Raw formula value to inspect.
* @param options Workbook-dependent parsing options.
* @returns
*/
const stringToNumberPatternCache = new FormulaAstLRU<{
@@ -314,14 +315,18 @@ const stringToNumberPatternCache = new FormulaAstLRU<{
pattern: string;
}>(100000);
export function stringIsNumberPattern(input: string) {
export function stringIsNumberPattern(
input: string,
options?: { dateSystem?: DateSystem }
) {
let _input = input;
if (_input.startsWith('"') && _input.endsWith('"')) {
_input = _input.slice(1, -1);
}
const cacheValue = stringToNumberPatternCache.get(_input);
const cacheKey = `${options?.dateSystem ?? ''}\u0000${_input}`;
const cacheValue = stringToNumberPatternCache.get(cacheKey);
if (cacheValue) {
return {
@@ -331,10 +336,10 @@ export function stringIsNumberPattern(input: string) {
};
}
const parseData = getNumfmtParseValueFilter(_input);
const parseData = getNumfmtParseValueFilter(_input, options);
if (parseData && parseData.z) {
return setNumberPatternCache(_input, parseData.v as number, parseData.z as string);
return setNumberPatternCache(cacheKey, parseData.v as number, parseData.z as string);
}
return {
@@ -14,6 +14,7 @@
* limitations under the License.
*/
import type { DateSystem } from '@univerjs/core';
import type { ArrayValueObject } from '../value-object/array-value-object';
import type { BaseValueObject } from '../value-object/base-value-object';
import { compareToken } from '../../basics/token';
@@ -21,7 +22,7 @@ import { ValueObjectFactory } from '../value-object/array-value-object';
import { BooleanValueObject, createBooleanValueObjectByRawValue } from '../value-object/primitive-object';
import { expandArrayValueObject } from './array-object';
export function findCompareToken(str: string): [compareToken, BaseValueObject] {
export function findCompareToken(str: string, dateSystem?: DateSystem): [compareToken, BaseValueObject] {
const comparisonTokens: compareToken[] = [
compareToken.EQUALS,
compareToken.NOT_EQUAL,
@@ -34,11 +35,11 @@ export function findCompareToken(str: string): [compareToken, BaseValueObject] {
for (const token of comparisonTokens) {
if (str.startsWith(token)) {
const content = str.substring(token.length);
return [token, ValueObjectFactory.create(content) as BaseValueObject];
return [token, ValueObjectFactory.create(content, false, dateSystem) as BaseValueObject];
}
}
return [compareToken.EQUALS, ValueObjectFactory.create(str) as BaseValueObject];
return [compareToken.EQUALS, ValueObjectFactory.create(str, false, dateSystem) as BaseValueObject];
}
/**
@@ -53,7 +54,10 @@ export function valueObjectCompare(range: BaseValueObject, criteria: BaseValueOb
// TODO: criteria: 32, ">32", B5, "3?", "apple*", "*~?", TODAY(), ">"&A1:B3
if (criteria.isString()) {
const criteriaValueString = `${criteria.getValue()}`;
const [token, criteriaStringObject] = findCompareToken(criteriaValueString);
const [token, criteriaStringObject] = findCompareToken(
criteriaValueString,
criteria.getDateSystem()
);
operator = token;
criteria = criteriaStringObject;
} else {
@@ -429,9 +429,13 @@ export function getPairedRangeAndCriteriaResult(
/**
* Two ArrayValueObject of the same type can be compared
*/
export function filterSameValueObjectResult(array: ArrayValueObject, range: ArrayValueObject, criteria: BaseValueObject): ArrayValueObject {
export function filterSameValueObjectResult(
array: ArrayValueObject,
range: ArrayValueObject,
criteria: BaseValueObject
): ArrayValueObject {
const [operator, criteriaObject] = criteria.isString()
? findCompareToken(`${criteria.getValue()}`)
? findCompareToken(`${criteria.getValue()}`, criteria.getDateSystem())
: [compareToken.EQUALS, criteria];
return array.mapValue((valueObject, r, c) => {
@@ -16,6 +16,7 @@
import type { Nullable } from '@univerjs/core';
import type { BaseValueObject } from '../base-value-object';
import { DateSystem } from '@univerjs/core';
import { describe, expect, it } from 'vitest';
import { ErrorType } from '../../../basics/error-type';
import { getObjectValue } from '../../../functions/util';
@@ -38,6 +39,38 @@ describe('arrayValueObject test', () => {
column: 0,
});
it('binds child values when the array already uses the requested date system', () => {
const array = ArrayValueObject.create({
calculateValueList: [[StringValueObject.create('1904-1-1')]],
rowCount: 1,
columnCount: 1,
unitId: '',
sheetId: '',
row: 0,
column: 0,
}, DateSystem.Date1904);
const value = array.withDateSystem(DateSystem.Date1904).get(0, 0) as BaseValueObject;
expect(value.convertToNumberObjectValue().getValue()).toBe(0);
});
it('binds the date system in place and updates child values', () => {
const array = ArrayValueObject.create({
calculateValueList: [[StringValueObject.create('1904-1-1')]],
rowCount: 1,
columnCount: 1,
unitId: '',
sheetId: '',
row: 0,
column: 0,
});
expect(array.withDateSystem(DateSystem.Date1904)).toBe(array);
expect(array.getDateSystem()).toBe(DateSystem.Date1904);
expect(array.get(0, 0)?.getDateSystem()).toBe(DateSystem.Date1904);
});
it('maps sparse comparison arrays with their default value', () => {
const sparseValues: BaseValueObject[][] = [];
sparseValues[1] = [BooleanValueObject.create(true)];
@@ -0,0 +1,56 @@
/**
* 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 { DateSystem } from '@univerjs/core';
import { describe, expect, it } from 'vitest';
import { NumberValueObject, StringValueObject } from '../primitive-object';
describe('StringValueObject', () => {
it('binds the date system in place for scalar values', () => {
const value = NumberValueObject.create(1);
expect(value.withDateSystem(DateSystem.Date1904)).toBe(value);
expect(value.getDateSystem()).toBe(DateSystem.Date1904);
});
it('uses the date system bound to the value for numeric conversion', () => {
const date1900Value = StringValueObject.create('1904-1-1').withDateSystem(DateSystem.Date1900);
const date1904Value = StringValueObject.create('1904-1-1').withDateSystem(DateSystem.Date1904);
expect(date1900Value.convertToNumberObjectValue().getValue()).toBe(1462);
expect(date1904Value.convertToNumberObjectValue().getValue()).toBe(0);
expect(date1900Value.convertToNumberObjectValue().getValue()).toBe(1462);
});
it('does not reuse a hyperlink object as a plain string', () => {
const hyperlink = StringValueObject.create('https://example.com/context-cache', {
isHyperlink: true,
hyperlinkUrl: 'https://example.com/context-cache',
});
const plainText = StringValueObject.create('https://example.com/context-cache');
expect(hyperlink.isHyperlink()).toBe(true);
expect(plainText.isHyperlink()).toBe(false);
});
it('does not share mutable metadata between plain strings', () => {
const first = StringValueObject.create('plain-string-cache-isolation').withCustomData({ source: 'first' });
const second = StringValueObject.create('plain-string-cache-isolation');
expect(second).not.toBe(first);
expect(second.getCustomData()).toBeUndefined();
});
});
@@ -16,7 +16,7 @@
import type { Nullable } from '@univerjs/core';
import type { callbackMapFnType, IArrayValueObject } from './base-value-object';
import { isRealNum } from '@univerjs/core';
import { DateSystem, isRealNum } from '@univerjs/core';
import { BooleanValue } from '../../basics/common';
import { ERROR_TYPE_SET, ErrorType } from '../../basics/error-type';
import { CELL_INVERTED_INDEX_CACHE } from '../../basics/inverted-index-cache';
@@ -58,7 +58,11 @@ enum ArrayCalculateType {
SINGLE,
}
export function transformToValueObject(array: Array<Array<number | string | boolean | null>> = [], isIgnoreNumberPattern: boolean = false) {
export function transformToValueObject(
array: Array<Array<number | string | boolean | null>> = [],
isIgnoreNumberPattern: boolean = false,
dateSystem?: DateSystem
) {
const arrayValueList: BaseValueObject[][] = [];
for (let r = 0; r < array.length; r++) {
@@ -71,7 +75,7 @@ export function transformToValueObject(array: Array<Array<number | string | bool
for (let c = 0; c < row.length; c++) {
const cell = row[c];
arrayValueList[r][c] = ValueObjectFactory.create(cell, isIgnoreNumberPattern);
arrayValueList[r][c] = ValueObjectFactory.create(cell, isIgnoreNumberPattern, dateSystem);
}
}
@@ -110,8 +114,8 @@ export class ArrayValueObject extends BaseValueObject {
* @param rawValue
* @returns
*/
static create(rawValue: string | IArrayValueObject) {
return new ArrayValueObject(rawValue);
static create(rawValue: string | IArrayValueObject, dateSystem?: DateSystem) {
return new ArrayValueObject(rawValue, dateSystem);
}
/**
@@ -168,10 +172,10 @@ export class ArrayValueObject extends BaseValueObject {
numberPosition: number[];
}>;
constructor(rawValue: string | IArrayValueObject) {
super();
constructor(rawValue: string | IArrayValueObject, dateSystem?: DateSystem) {
super(dateSystem);
this._values = this._formatValue(rawValue);
this._values = this._formatValue(rawValue, dateSystem);
}
override dispose(): void {
@@ -957,6 +961,14 @@ export class ArrayValueObject extends BaseValueObject {
return accumulatorAll;
}
override withDateSystem(dateSystem: DateSystem): this {
super.withDateSystem(dateSystem);
// Child values read their own date system during parsing and formatting.
this._values.forEach((row) => row.forEach((valueObject) => valueObject?.withDateSystem(dateSystem)));
this._defaultValue?.withDateSystem(dateSystem);
return this;
}
override getNegative(): BaseValueObject {
return this.mapValue((valueObject) => NumberValueObject.create(0).minus(valueObject));
}
@@ -1902,7 +1914,7 @@ export class ArrayValueObject extends BaseValueObject {
return ArrayCalculateType.PRODUCT;
}
private _formatValue(rawValue: string | IArrayValueObject) {
private _formatValue(rawValue: string | IArrayValueObject, dateSystem?: DateSystem) {
if (typeof rawValue !== 'string') {
rawValue = rawValue as IArrayValueObject;
@@ -1941,7 +1953,7 @@ export class ArrayValueObject extends BaseValueObject {
const row: BaseValueObject[] = [];
for (let c = 0; c < columnArrayCount; c++) {
const cellRaw = columnArray[c].trim();
row.push(ValueObjectFactory.create(cellRaw));
row.push(ValueObjectFactory.create(cellRaw, false, dateSystem));
}
result.push(row);
}
@@ -1975,12 +1987,16 @@ export class ArrayValueObject extends BaseValueObject {
column,
};
return ArrayValueObject.create(arrayValueObjectData);
return ArrayValueObject.create(arrayValueObjectData, this.getDateSystem());
}
}
export class ValueObjectFactory {
static create(rawValue: string | number | boolean | null, isIgnoreNumberPattern: boolean = false): BaseValueObject {
static create(
rawValue: string | number | boolean | null,
isIgnoreNumberPattern: boolean = false,
dateSystem: DateSystem = DateSystem.Date1900
): BaseValueObject {
if (rawValue == null) {
return NullValueObject.create();
}
@@ -1997,31 +2013,32 @@ export class ValueObjectFactory {
}
if (isStringWrappedByDoubleQuotes(rawValue)) {
return createStringValueObjectByRawValue(rawValue);
return createStringValueObjectByRawValue(rawValue).withDateSystem(dateSystem);
}
// "000123456" should be treated as string, not number, but "123456" should be treated as number
if (isRealNum(rawValue) && Number(rawValue).toString() === rawValue.trim()) {
return NumberValueObject.create(Number(rawValue));
return NumberValueObject.create(Number(rawValue)).withDateSystem(dateSystem);
}
// value ignore whether it is a number pattern
if (!isIgnoreNumberPattern) {
const { isNumberPattern, value, pattern } = stringIsNumberPattern(rawValue);
// Date text must be converted to a serial using the current workbook date system.
const { isNumberPattern, value, pattern } = stringIsNumberPattern(rawValue, { dateSystem });
if (isNumberPattern) {
return NumberValueObject.create(value as number, pattern as string);
return NumberValueObject.create(value as number, pattern as string).withDateSystem(dateSystem);
}
}
const rawValueSingleLine = rawValue.replace(/\n/g, '').replace(/\r/g, '');
if (!isStringWrappedByDoubleQuotes(rawValueSingleLine) && regexTestArrayValue(rawValueSingleLine)) {
return ArrayValueObject.create(rawValueSingleLine);
return ArrayValueObject.create(rawValueSingleLine, dateSystem);
}
return createStringValueObjectByRawValue(rawValue);
return createStringValueObjectByRawValue(rawValue).withDateSystem(dateSystem);
}
if (typeof rawValue === 'number') {
return createNumberValueObjectByRawValue(rawValue);
return createNumberValueObjectByRawValue(rawValue).withDateSystem(dateSystem);
}
return ErrorValueObject.create(ErrorType.VALUE);
}
@@ -15,6 +15,7 @@
*/
import type { CustomData, Nullable } from '@univerjs/core';
import { DateSystem } from '@univerjs/core';
import { FormulaAstLRU } from '../../basics/cache-lru';
import { ConcatenateType } from '../../basics/common';
import { ErrorType } from '../../basics/error-type';
@@ -36,6 +37,12 @@ export interface IArrayValueObject {
}
export class BaseValueObject extends ObjectClassType {
private _customData: CustomData;
private _dateSystem: DateSystem;
constructor(dateSystem: DateSystem = DateSystem.Date1900) {
super();
this._dateSystem = dateSystem;
}
override isValueObject() {
return true;
@@ -81,6 +88,16 @@ export class BaseValueObject extends ObjectClassType {
return this._customData;
}
getDateSystem() {
return this._dateSystem;
}
/** Bind the current calculation context in place before the value is used. */
withDateSystem(dateSystem: DateSystem): this {
this._dateSystem = dateSystem;
return this;
}
isCube() {
return false;
}
@@ -116,8 +116,9 @@ export class LambdaValueObjectObject extends BaseValueObject {
* @param variants
*/
executeCustom(...variants: PrimitiveValueType[]) {
// Create base value object from primitive value, then execute
const baseValueObjects = variants.map((variant) => ValueObjectFactory.create(variant));
const dateSystem = this._interpreter?.getDateSystem();
// Primitive arguments must be converted before nested execution, using the same workbook context.
const baseValueObjects = variants.map((variant) => ValueObjectFactory.create(variant, false, dateSystem));
return this.execute(...baseValueObjects);
}
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { getNumfmtParseValueFilter, isRealNum, numfmt, Tools } from '@univerjs/core';
import { DateSystem, getNumfmtParseValueFilter, isRealNum, numfmt, Tools } from '@univerjs/core';
import { FormulaAstLRU } from '../../basics/cache-lru';
import { reverseCompareOperator } from '../../basics/calculate';
import { BooleanValue, ConcatenateType } from '../../basics/common';
@@ -447,8 +447,8 @@ export class NumberValueObject extends BaseValueObject {
return instance;
}
constructor(rawValue: number) {
super();
constructor(rawValue: number, dateSystem: DateSystem = DateSystem.Date1900) {
super(dateSystem);
this._value = Number(rawValue);
}
@@ -1432,8 +1432,8 @@ export class StringValueObject extends BaseValueObject {
return true;
};
constructor(rawValue: string) {
super();
constructor(rawValue: string, dateSystem: DateSystem = DateSystem.Date1900) {
super(dateSystem);
this._value = rawValue;
}
@@ -1581,13 +1581,13 @@ export class StringValueObject extends BaseValueObject {
return ErrorValueObject.create(ErrorType.VALUE);
}
const parseData = getNumfmtParseValueFilter(rawValue);
const parseData = getNumfmtParseValueFilter(rawValue, { dateSystem: this.getDateSystem() });
if (parseData && parseData.z) {
return createNumberValueObjectByRawValue(parseData.v, parseData.z);
return createNumberValueObjectByRawValue(parseData.v, parseData.z).withDateSystem(this.getDateSystem());
}
return createNumberValueObjectByRawValue(rawValue);
return createNumberValueObjectByRawValue(rawValue).withDateSystem(this.getDateSystem());
}
override convertToBooleanObjectValue() {
@@ -29,6 +29,7 @@ import type { FormulaFunctionResultValueType, FormulaFunctionValueType } from '.
import type { FormulaDataModel } from '../models/formula-data.model';
import type { IDefinedNameMapItem } from '../services/defined-names.service';
import type { IFormulaUnitReferenceResolver } from '../services/unit-reference-resolver.service';
import { DateSystem } from '@univerjs/core';
import { ErrorType } from '../basics/error-type';
import { regexTestSingeRange, regexTestSingleColumn, regexTestSingleRow } from '../basics/regex';
import { compareToken } from '../basics/token';
@@ -52,6 +53,7 @@ export class BaseFunction {
private _currentFormulaColumnCount: number = 1;
private _definedNames: Nullable<IDefinedNameMapItem>;
private _locale: LocaleType;
private _dateSystem = DateSystem.Date1900;
private _sheetOrder: string[];
private _sheetNameMap: { [sheetId: string]: string };
protected _formulaDataModel: Nullable<FormulaDataModel>;
@@ -188,6 +190,14 @@ export class BaseFunction {
this._locale = locale;
}
getDateSystem() {
return this._dateSystem;
}
setDateSystem(dateSystem: DateSystem) {
this._dateSystem = dateSystem;
}
getSheetsInfo() {
return {
sheetOrder: this._sheetOrder,
@@ -55,7 +55,7 @@ export class Daverage extends BaseFunction {
continue;
}
if (isCriteriaMatch(criteriaValues, databaseValues, r)) {
if (isCriteriaMatch(criteriaValues, databaseValues, r, criteria.getDateSystem())) {
sum += value;
count++;
}
@@ -52,7 +52,7 @@ export class Dcount extends BaseFunction {
continue;
}
if (isCriteriaMatch(criteriaValues, databaseValues, r)) {
if (isCriteriaMatch(criteriaValues, databaseValues, r, criteria.getDateSystem())) {
count++;
}
}
@@ -52,7 +52,7 @@ export class Dcounta extends BaseFunction {
continue;
}
if (isCriteriaMatch(criteriaValues, databaseValues, r)) {
if (isCriteriaMatch(criteriaValues, databaseValues, r, criteria.getDateSystem())) {
count++;
}
}
@@ -55,7 +55,7 @@ export class Dget extends BaseFunction {
continue;
}
if (isCriteriaMatch(criteriaValues, databaseValues, r)) {
if (isCriteriaMatch(criteriaValues, databaseValues, r, criteria.getDateSystem())) {
count++;
resultRowIndex = r;
}
@@ -53,7 +53,7 @@ export class Dmax extends BaseFunction {
continue;
}
if (isCriteriaMatch(criteriaValues, databaseValues, r)) {
if (isCriteriaMatch(criteriaValues, databaseValues, r, criteria.getDateSystem())) {
result = Math.max(result, value);
count++;
}
@@ -53,7 +53,7 @@ export class Dmin extends BaseFunction {
continue;
}
if (isCriteriaMatch(criteriaValues, databaseValues, r)) {
if (isCriteriaMatch(criteriaValues, databaseValues, r, criteria.getDateSystem())) {
result = Math.min(result, value);
count++;
}
@@ -53,7 +53,7 @@ export class Dproduct extends BaseFunction {
continue;
}
if (isCriteriaMatch(criteriaValues, databaseValues, r)) {
if (isCriteriaMatch(criteriaValues, databaseValues, r, criteria.getDateSystem())) {
result *= value;
count++;
}
@@ -57,7 +57,7 @@ export class Dstdev extends BaseFunction {
continue;
}
if (isCriteriaMatch(criteriaValues, databaseValues, r)) {
if (isCriteriaMatch(criteriaValues, databaseValues, r, criteria.getDateSystem())) {
values.push(value);
sum += value;
count++;
@@ -57,7 +57,7 @@ export class Dstdevp extends BaseFunction {
continue;
}
if (isCriteriaMatch(criteriaValues, databaseValues, r)) {
if (isCriteriaMatch(criteriaValues, databaseValues, r, criteria.getDateSystem())) {
values.push(value);
sum += value;
count++;
@@ -14,6 +14,7 @@
* limitations under the License.
*/
import { DateSystem } from '@univerjs/core';
import { describe, expect, it } from 'vitest';
import { ErrorType } from '../../../../basics/error-type';
import { ArrayValueObject, transformToValueObject } from '../../../../engine/value-object/array-value-object';
@@ -62,6 +63,41 @@ describe('Test dsum function', () => {
expect(getObjectValue(result)).toStrictEqual(24);
});
it('uses the date system for date criteria comparisons', () => {
const database = ArrayValueObject.create({
calculateValueList: transformToValueObject([
['日期', '金额'],
[0, 10],
[1, 20],
]),
rowCount: 3,
columnCount: 2,
unitId: '',
sheetId: '',
row: 0,
column: 0,
});
const field = StringValueObject.create('金额');
const createCriteria = () => ArrayValueObject.create({
calculateValueList: transformToValueObject([
['日期'],
['>1904-01-01'],
]),
rowCount: 2,
columnCount: 1,
unitId: '',
sheetId: '',
row: 0,
column: 0,
});
const criteria1900 = createCriteria().withDateSystem(DateSystem.Date1900);
expect(getObjectValue(testFunction.calculate(database, field, criteria1900))).toBe(0);
const criteria1904 = createCriteria().withDateSystem(DateSystem.Date1904);
expect(getObjectValue(testFunction.calculate(database, field, criteria1904))).toBe(20);
});
it('Database value test', () => {
const database = ArrayValueObject.create({
calculateValueList: transformToValueObject([
@@ -52,7 +52,7 @@ export class Dsum extends BaseFunction {
continue;
}
if (isCriteriaMatch(criteriaValues, databaseValues, r)) {
if (isCriteriaMatch(criteriaValues, databaseValues, r, criteria.getDateSystem())) {
result += value;
}
}
@@ -57,7 +57,7 @@ export class Dvar extends BaseFunction {
continue;
}
if (isCriteriaMatch(criteriaValues, databaseValues, r)) {
if (isCriteriaMatch(criteriaValues, databaseValues, r, criteria.getDateSystem())) {
values.push(value);
sum += value;
count++;
@@ -57,7 +57,7 @@ export class Dvarp extends BaseFunction {
continue;
}
if (isCriteriaMatch(criteriaValues, databaseValues, r)) {
if (isCriteriaMatch(criteriaValues, databaseValues, r, criteria.getDateSystem())) {
values.push(value);
sum += value;
count++;
@@ -14,6 +14,7 @@
* limitations under the License.
*/
import { DateSystem } from '@univerjs/core';
import { describe, expect, it } from 'vitest';
import { ErrorType } from '../../../../basics/error-type';
import { ArrayValueObject, transformToValueObject } from '../../../../engine/value-object/array-value-object';
@@ -50,6 +51,28 @@ describe('Test date function', () => {
expect(getObjectValue(result)).toBe(0);
});
it('preserves Excel 1900 leap-year compatibility dates', () => {
const month = NumberValueObject.create(2);
expect(getObjectValue(testFunction.calculate(NumberValueObject.create(1900), month, NumberValueObject.create(29)))).toBe(60);
expect(getObjectValue(testFunction.calculate(NumberValueObject.create(1900), month, NumberValueObject.create(30)))).toBe(61);
});
it('uses the configured Excel date system', () => {
const month = NumberValueObject.create(1);
const day = NumberValueObject.create(1);
testFunction.setDateSystem(DateSystem.Date1900);
expect(getObjectValue(testFunction.calculate(NumberValueObject.create(1904), month, day))).toBe(1462);
expect(getObjectValue(testFunction.calculate(NumberValueObject.create(2000), month, day))).toBe(36526);
testFunction.setDateSystem(DateSystem.Date1904);
expect(getObjectValue(testFunction.calculate(NumberValueObject.create(1904), month, day))).toBe(0);
expect(getObjectValue(testFunction.calculate(NumberValueObject.create(2000), month, day))).toBe(35064);
testFunction.setDateSystem(DateSystem.Date1900);
});
it('Edge case, 1900.1.-1', () => {
const year = NumberValueObject.create(1900);
const month = NumberValueObject.create(1);
@@ -16,7 +16,8 @@
import type { ArrayValueObject } from '../../../engine/value-object/array-value-object';
import type { BaseValueObject } from '../../../engine/value-object/base-value-object';
import { DEFAULT_DATE_FORMAT, excelDateSerial } from '../../../basics/date';
import { excelDateSerial } from '@univerjs/core';
import { DEFAULT_DATE_FORMAT } from '../../../basics/date';
import { ErrorType } from '../../../basics/error-type';
import { expandArrayValueObject } from '../../../engine/utils/array-object';
import { checkVariantsErrorIsStringToNumber } from '../../../engine/utils/check-variant-error';
@@ -81,9 +82,11 @@ export class DateFunction extends BaseFunction {
yearValue += 1900;
}
const date = new Date(Date.UTC(yearValue, monthValue - 1, dayValue));
const currentSerial = excelDateSerial(date);
// Build the serial from the first day of the normalized month. This keeps
// Excel's fictitious 1900-02-29 (serial 60) instead of letting native Date
// normalize it to 1900-03-01.
const firstDateOfMonth = new Date(Date.UTC(yearValue, monthValue - 1, 1));
const currentSerial = excelDateSerial(firstDateOfMonth, this.getDateSystem()) + dayValue - 1;
if (currentSerial < 0) {
return ErrorValueObject.create(ErrorType.NUM);
@@ -14,6 +14,7 @@
* limitations under the License.
*/
import { DateSystem } from '@univerjs/core';
import { describe, expect, it } from 'vitest';
import { ErrorType } from '../../../../basics/error-type';
import { ArrayValueObject, transformToValueObject } from '../../../../engine/value-object/array-value-object';
@@ -55,6 +56,28 @@ describe('Test datedif function', () => {
expect(result.getValue()).toStrictEqual(61);
});
it('uses the configured date system for numeric serials', () => {
const startDate = NumberValueObject.create(0);
const endDate = NumberValueObject.create(60);
const unit = StringValueObject.create('M');
testFunction.setDateSystem(DateSystem.Date1900);
expect(testFunction.calculate(startDate, endDate, unit).getValue()).toBe(1);
testFunction.setDateSystem(DateSystem.Date1904);
expect(testFunction.calculate(startDate, endDate, unit).getValue()).toBe(2);
testFunction.setDateSystem(DateSystem.Date1900);
});
it('handles the 1900 leap-year boundary serial without normalizing it away', () => {
testFunction.setDateSystem(DateSystem.Date1900);
const result = testFunction.calculate(NumberValueObject.create(60), NumberValueObject.create(90), StringValueObject.create('MD'));
expect(result.getValue()).toBe(1);
});
it('value is normal, endDate < startDate', () => {
const startDate = StringValueObject.create('2021/3/31');
const endDate = StringValueObject.create('2011/1/29');
@@ -241,5 +264,20 @@ describe('Test datedif function', () => {
const result3 = testFunction.calculate(startDate3, endDate3, unit);
expect(result3.getValue()).toStrictEqual(364);
});
it('checks serial-zero day and year differences in both date systems', () => {
const startDate = NumberValueObject.create(0);
const endDate = NumberValueObject.create(366);
testFunction.setDateSystem(DateSystem.Date1900);
expect(testFunction.calculate(startDate, endDate, StringValueObject.create('D')).getValue()).toBe(366);
expect(testFunction.calculate(startDate, endDate, StringValueObject.create('Y')).getValue()).toBe(0);
testFunction.setDateSystem(DateSystem.Date1904);
expect(testFunction.calculate(startDate, endDate, StringValueObject.create('D')).getValue()).toBe(366);
expect(testFunction.calculate(startDate, endDate, StringValueObject.create('Y')).getValue()).toBe(1);
testFunction.setDateSystem(DateSystem.Date1900);
});
});
});
@@ -16,7 +16,8 @@
import type { ArrayValueObject } from '../../../engine/value-object/array-value-object';
import type { BaseValueObject } from '../../../engine/value-object/base-value-object';
import { excelDateSerial, excelSerialToDate, getDateSerialNumberByObject, getDaysInMonth } from '../../../basics/date';
import { DateSystem, excelDateSerial, excelSerialToDate, excelSerialToDateTimeParts } from '@univerjs/core';
import { getDateSerialNumberByObject, getDaysInMonth } from '../../../basics/date';
import { ErrorType } from '../../../basics/error-type';
import { ErrorValueObject } from '../../../engine/value-object/base-value-object';
import { NumberValueObject } from '../../../engine/value-object/primitive-object';
@@ -56,13 +57,13 @@ export class Datedif extends BaseFunction {
return _unit;
}
const startDateSerialNumber = getDateSerialNumberByObject(_startDate);
const startDateSerialNumber = getDateSerialNumberByObject(_startDate, this.getDateSystem());
if (typeof startDateSerialNumber !== 'number') {
return startDateSerialNumber;
}
const endDateSerialNumber = getDateSerialNumberByObject(_endDate);
const endDateSerialNumber = getDateSerialNumberByObject(_endDate, this.getDateSystem());
if (typeof endDateSerialNumber !== 'number') {
return endDateSerialNumber;
@@ -80,15 +81,20 @@ export class Datedif extends BaseFunction {
}
private _getResultByUnit(startDateSerialNumber: number, endDateSerialNumber: number, unit: BaseValueObject): BaseValueObject {
const startDateDate = excelSerialToDate(startDateSerialNumber);
const startYear = startDateDate.getUTCFullYear();
const startMonth = startDateDate.getUTCMonth() + 1;
const startDay = startDateDate.getUTCDate();
const startDateDate = excelSerialToDate(startDateSerialNumber, this.getDateSystem());
// Native Date cannot represent Excel's fictitious 1900-02-29 (serial 60), so use
// the decoded Excel fields for calendar calculations that depend on the day value.
const startDateParts = excelSerialToDateTimeParts(startDateSerialNumber, { dateSystem: this.getDateSystem() });
const startIs1900SerialZero = this.getDateSystem() === DateSystem.Date1900 && startDateSerialNumber === 0;
const startYear = startIs1900SerialZero ? 1900 : startDateParts?.year ?? startDateDate.getUTCFullYear();
const startMonth = startIs1900SerialZero ? 1 : startDateParts?.month ?? startDateDate.getUTCMonth() + 1;
const startDay = startIs1900SerialZero ? 0 : startDateParts?.day ?? startDateDate.getUTCDate();
const endDateDate = excelSerialToDate(endDateSerialNumber);
const endYear = endDateDate.getUTCFullYear();
const endMonth = endDateDate.getUTCMonth() + 1;
const endDay = endDateDate.getUTCDate();
const endDateDate = excelSerialToDate(endDateSerialNumber, this.getDateSystem());
const endDateParts = excelSerialToDateTimeParts(endDateSerialNumber, { dateSystem: this.getDateSystem() });
const endYear = endDateParts?.year ?? endDateDate.getUTCFullYear();
const endMonth = endDateParts?.month ?? endDateDate.getUTCMonth() + 1;
const endDay = endDateParts?.day ?? endDateDate.getUTCDate();
const unitValue = `${unit.getValue()}`.toLocaleUpperCase();
@@ -146,7 +152,7 @@ export class Datedif extends BaseFunction {
newDate = new Date(Date.UTC(startYear + 1, endMonth - 1, endDay));
}
diff = Math.floor(excelDateSerial(newDate)) - Math.floor(startDateSerialNumber);
diff = Math.floor(excelDateSerial(newDate, this.getDateSystem())) - Math.floor(startDateSerialNumber);
break;
default:
return ErrorValueObject.create(ErrorType.NUM);
@@ -14,8 +14,8 @@
* limitations under the License.
*/
import { DateSystem, excelDateSerial } from '@univerjs/core';
import { describe, expect, it } from 'vitest';
import { excelDateSerial } from '../../../../basics/date';
import { ErrorType } from '../../../../basics/error-type';
import { ArrayValueObject, transformToValueObject } from '../../../../engine/value-object/array-value-object';
import { ErrorValueObject } from '../../../../engine/value-object/base-value-object';
@@ -46,6 +46,18 @@ describe('Test datevalue function', () => {
expect(getObjectValue(result4)).toStrictEqual(0);
});
it('uses the configured Excel date system', () => {
testFunction.setDateSystem(DateSystem.Date1900);
expect(getObjectValue(testFunction.calculate(StringValueObject.create('1904-1-1')))).toBe(1462);
expect(getObjectValue(testFunction.calculate(StringValueObject.create('1904-2-2')))).toBe(1494);
testFunction.setDateSystem(DateSystem.Date1904);
expect(getObjectValue(testFunction.calculate(StringValueObject.create('1904-1-1')))).toBe(0);
expect(getObjectValue(testFunction.calculate(StringValueObject.create('1904-2-2')))).toBe(32);
testFunction.setDateSystem(DateSystem.Date1900);
});
it('Date text number', () => {
const dateText = NumberValueObject.create(43832);
const result = testFunction.calculate(dateText);
@@ -15,7 +15,8 @@
*/
import type { BaseValueObject } from '../../../engine/value-object/base-value-object';
import { excelDateTimeSerial, isDate, parseFormattedDate, parseFormattedTime } from '../../../basics/date';
import { excelDateTimeSerial } from '@univerjs/core';
import { isDate, parseFormattedDate, parseFormattedTime } from '../../../basics/date';
import { ErrorType } from '../../../basics/error-type';
import { ErrorValueObject } from '../../../engine/value-object/base-value-object';
import { NumberValueObject } from '../../../engine/value-object/primitive-object';
@@ -45,9 +46,10 @@ export class Datevalue extends BaseFunction {
if (dateTextObject.isString()) {
const value = `${dateTextObject.getValue()}`;
let parsedDate = parseFormattedDate(value);
const options = { dateSystem: this.getDateSystem() };
let parsedDate = parseFormattedDate(value, options);
if (parsedDate === null) {
parsedDate = parseFormattedTime(value);
parsedDate = parseFormattedTime(value, options);
}
if (parsedDate) {
let { v, z } = parsedDate;
@@ -55,7 +57,7 @@ export class Datevalue extends BaseFunction {
// currently, we the v is a number by numfmt the 3.2
if (z && isDate(z)) {
if ((v as any) instanceof Date) {
v = excelDateTimeSerial(v as any);
v = excelDateTimeSerial(v as any, this.getDateSystem());
}
return NumberValueObject.create(Math.trunc(+v));
@@ -14,6 +14,7 @@
* limitations under the License.
*/
import { DateSystem } from '@univerjs/core';
import { describe, expect, it } from 'vitest';
import { ErrorType } from '../../../../basics/error-type';
import { ArrayValueObject, transformToValue, transformToValueObject } from '../../../../engine/value-object/array-value-object';
@@ -31,6 +32,18 @@ describe('Test day function', () => {
const result = testFunction.calculate(serialNumber);
expect(result.getValue()).toStrictEqual(2);
});
it('uses the configured date system at the epoch boundary', () => {
const serialNumber = NumberValueObject.create(0);
testFunction.setDateSystem(DateSystem.Date1900);
expect(testFunction.calculate(serialNumber).getValue()).toBe(0);
testFunction.setDateSystem(DateSystem.Date1904);
expect(testFunction.calculate(serialNumber).getValue()).toBe(1);
testFunction.setDateSystem(DateSystem.Date1900);
});
it('Serial number is error', () => {
const serialNumber = ErrorValueObject.create(ErrorType.NAME);
const result = testFunction.calculate(serialNumber);
@@ -15,7 +15,8 @@
*/
import type { BaseValueObject } from '../../../engine/value-object/base-value-object';
import { excelSerialToDate, isValidDateStr } from '../../../basics/date';
import { excelSerialToDateTimeParts } from '@univerjs/core';
import { isValidDateStr } from '../../../basics/date';
import { ErrorType } from '../../../basics/error-type';
import { ErrorValueObject } from '../../../engine/value-object/base-value-object';
import { NumberValueObject } from '../../../engine/value-object/primitive-object';
@@ -55,13 +56,8 @@ export class Day extends BaseFunction {
return ErrorValueObject.create(ErrorType.NUM);
}
// Excel serial 0 is 1900-01-00
// Google Sheets serial 0 is 1899-12-30
if (dateSerial === 0) {
return NumberValueObject.create(0);
}
date = excelSerialToDate(dateSerial);
const parts = excelSerialToDateTimeParts(dateSerial, { dateSystem: this.getDateSystem() });
return parts ? NumberValueObject.create(parts.day) : ErrorValueObject.create(ErrorType.NUM);
}
const day = date.getUTCDate();
@@ -14,6 +14,7 @@
* limitations under the License.
*/
import { DateSystem } from '@univerjs/core';
import { describe, expect, it } from 'vitest';
import { ErrorType } from '../../../../basics/error-type';
import { ArrayValueObject, transformToValue, transformToValueObject } from '../../../../engine/value-object/array-value-object';
@@ -91,5 +92,17 @@ describe('Test days function', () => {
[40950, ErrorType.VALUE, 0, -1, ErrorType.NAME, -1],
]);
});
it('uses the configured date system for serial-zero inputs', () => {
const startDate = NumberValueObject.create(0);
const endDate = NumberValueObject.create(366);
testFunction.setDateSystem(DateSystem.Date1900);
expect(testFunction.calculate(endDate, startDate).getValue()).toBe(366);
testFunction.setDateSystem(DateSystem.Date1904);
expect(testFunction.calculate(endDate, startDate).getValue()).toBe(366);
testFunction.setDateSystem(DateSystem.Date1900);
});
});
});
@@ -59,13 +59,13 @@ export class Days extends BaseFunction {
return startDateObject;
}
const endDateSerialNumber = getDateSerialNumberByObject(endDateObject);
const endDateSerialNumber = getDateSerialNumberByObject(endDateObject, this.getDateSystem());
if (typeof endDateSerialNumber !== 'number') {
return endDateSerialNumber;
}
const startDateSerialNumber = getDateSerialNumberByObject(startDateObject);
const startDateSerialNumber = getDateSerialNumberByObject(startDateObject, this.getDateSystem());
if (typeof startDateSerialNumber !== 'number') {
return startDateSerialNumber;
@@ -14,6 +14,7 @@
* limitations under the License.
*/
import { DateSystem } from '@univerjs/core';
import { describe, expect, it } from 'vitest';
import { ErrorType } from '../../../../basics/error-type';
import { ArrayValueObject, transformToValue, transformToValueObject } from '../../../../engine/value-object/array-value-object';
@@ -38,6 +39,19 @@ describe('Test days360 function', () => {
expect(result.getValue()).toStrictEqual(61);
});
it('uses the configured date system for numeric serials', () => {
const startDate = NumberValueObject.create(0);
const endDate = NumberValueObject.create(31);
testFunction.setDateSystem(DateSystem.Date1900);
expect(testFunction.calculate(startDate, endDate).getValue()).toBe(31);
testFunction.setDateSystem(DateSystem.Date1904);
expect(testFunction.calculate(startDate, endDate).getValue()).toBe(30);
testFunction.setDateSystem(DateSystem.Date1900);
});
it('value is normal, endDate < startDate', () => {
const startDate = StringValueObject.create('2021/3/31');
const endDate = StringValueObject.create('2021/1/29');
@@ -126,5 +140,18 @@ describe('Test days360 function', () => {
[ErrorType.NA, ErrorType.VALUE, ErrorType.NA, ErrorType.NA, ErrorType.NAME],
]);
});
it('checks the serial-zero boundary in both date systems', () => {
const startDate = NumberValueObject.create(0);
const endDate = NumberValueObject.create(366);
const method = NumberValueObject.create(0);
testFunction.setDateSystem(DateSystem.Date1900);
expect(testFunction.calculate(startDate, endDate, method).getValue()).toBe(361);
testFunction.setDateSystem(DateSystem.Date1904);
expect(testFunction.calculate(startDate, endDate, method).getValue()).toBe(360);
testFunction.setDateSystem(DateSystem.Date1900);
});
});
});
@@ -67,7 +67,7 @@ export class Days360 extends BaseFunction {
return startDateObject;
}
const startDateSerialNumber = getDateSerialNumberByObject(startDateObject);
const startDateSerialNumber = getDateSerialNumberByObject(startDateObject, this.getDateSystem());
if (typeof startDateSerialNumber !== 'number') {
return startDateSerialNumber;
@@ -77,7 +77,7 @@ export class Days360 extends BaseFunction {
return endDateObject;
}
const endDateSerialNumber = getDateSerialNumberByObject(endDateObject);
const endDateSerialNumber = getDateSerialNumberByObject(endDateObject, this.getDateSystem());
if (typeof endDateSerialNumber !== 'number') {
return endDateSerialNumber;
@@ -93,7 +93,7 @@ export class Days360 extends BaseFunction {
const methodValue = +methodObject.getValue();
const { days } = getTwoDateDaysByBasis(startDateSerialNumber, endDateSerialNumber, !methodValue ? 0 : 4);
const { days } = getTwoDateDaysByBasis(startDateSerialNumber, endDateSerialNumber, !methodValue ? 0 : 4, this.getDateSystem());
const result = endDateSerialNumber >= startDateSerialNumber ? days : -days;
@@ -14,8 +14,8 @@
* limitations under the License.
*/
import { DateSystem, excelDateSerial } from '@univerjs/core';
import { describe, expect, it } from 'vitest';
import { excelDateSerial } from '../../../../basics/date';
import { ArrayValueObject, transformToValue, transformToValueObject } from '../../../../engine/value-object/array-value-object';
import { NumberValueObject } from '../../../../engine/value-object/primitive-object';
import { FUNCTION_NAMES_DATE } from '../../function-names';
@@ -32,6 +32,19 @@ describe('Test edate function', () => {
expect(transformToValue(result.getArrayValue())).toStrictEqual([[43862]]);
});
it('uses the configured date system for serials around the epoch', () => {
const startDate = NumberValueObject.create(31);
const months = NumberValueObject.create(1);
testFunction.setDateSystem(DateSystem.Date1900);
expect(transformToValue(testFunction.calculate(startDate, months).getArrayValue())).toStrictEqual([[59]]);
testFunction.setDateSystem(DateSystem.Date1904);
expect(transformToValue(testFunction.calculate(startDate, months).getArrayValue())).toStrictEqual([[60]]);
testFunction.setDateSystem(DateSystem.Date1900);
});
it('Clamps month-end overflow to the target month last day', () => {
const startDate = NumberValueObject.create(excelDateSerial(new Date(Date.UTC(2025, 7, 31))));
const months = NumberValueObject.create(-2);
@@ -108,5 +121,26 @@ describe('Test edate function', () => {
const result = testFunction.calculate(startDate, months);
expect(transformToValue(result.getArrayValue())).toStrictEqual([[43862, '#VALUE!', 43862, '#VALUE!', '#VALUE!', 43831], [43831, 46874, 43891, '#VALUE!', 43739, 101660]]);
});
it('checks the serial-zero boundary in both date systems', () => {
const startDate = NumberValueObject.create(0);
const months = NumberValueObject.create(1);
testFunction.setDateSystem(DateSystem.Date1900);
expect(transformToValue(testFunction.calculate(startDate, months).getArrayValue())).toStrictEqual([[31]]);
testFunction.setDateSystem(DateSystem.Date1904);
expect(transformToValue(testFunction.calculate(startDate, months).getArrayValue())).toStrictEqual([[31]]);
testFunction.setDateSystem(DateSystem.Date1900);
});
it('preserves Excel serial 60 when no month is added', () => {
testFunction.setDateSystem(DateSystem.Date1900);
const result = testFunction.calculate(NumberValueObject.create(60), NumberValueObject.create(0));
expect(transformToValue(result.getArrayValue())).toStrictEqual([[59]]);
});
});
});
@@ -16,7 +16,8 @@
import type { ArrayValueObject } from '../../../engine/value-object/array-value-object';
import type { BaseValueObject } from '../../../engine/value-object/base-value-object';
import { DEFAULT_DATE_FORMAT, excelDateSerial, excelSerialToDate } from '../../../basics/date';
import { excelDateSerial, excelSerialToDate } from '@univerjs/core';
import { DEFAULT_DATE_FORMAT } from '../../../basics/date';
import { ErrorType } from '../../../basics/error-type';
import { expandArrayValueObject } from '../../../engine/utils/array-object';
import { ErrorValueObject } from '../../../engine/value-object/base-value-object';
@@ -78,7 +79,7 @@ export class Edate extends BaseFunction {
const monthsValue = Math.floor(+monthsValueObject.getValue());
const _startDate = excelSerialToDate(startDateSerial);
const _startDate = excelSerialToDate(startDateSerial, this.getDateSystem());
const year = _startDate.getUTCFullYear();
const month = _startDate.getUTCMonth() + monthsValue;
@@ -86,7 +87,7 @@ export class Edate extends BaseFunction {
const lastDayOfTargetMonth = new Date(Date.UTC(year, month + 1, 0)).getUTCDate();
const resultDate = new Date(Date.UTC(year, month, Math.min(day, lastDayOfTargetMonth)));
const currentSerial = excelDateSerial(resultDate);
const currentSerial = excelDateSerial(resultDate, this.getDateSystem());
return NumberValueObject.create(currentSerial, DEFAULT_DATE_FORMAT);
});
@@ -14,6 +14,7 @@
* limitations under the License.
*/
import { DateSystem } from '@univerjs/core';
import { describe, expect, it } from 'vitest';
import { ErrorType } from '../../../../basics/error-type';
import { ArrayValueObject, transformToValueObject } from '../../../../engine/value-object/array-value-object';
@@ -71,6 +72,16 @@ describe('Test eomonth function', () => {
expect(result.getValue()).toStrictEqual(42429);
});
it('treats serial zero as 1904-01-01 in the 1904 date system', () => {
testFunction.setDateSystem(DateSystem.Date1900);
expect(testFunction.calculate(NumberValueObject.create(0), NumberValueObject.create(0)).getValue()).toBe(31);
expect(testFunction.calculate(NumberValueObject.create(31), NumberValueObject.create(0)).getValue()).toBe(31);
testFunction.setDateSystem(DateSystem.Date1904);
expect(testFunction.calculate(NumberValueObject.create(0), NumberValueObject.create(0)).getValue()).toBe(30);
expect(testFunction.calculate(NumberValueObject.create(31), NumberValueObject.create(0)).getValue()).toBe(59);
testFunction.setDateSystem(DateSystem.Date1900);
});
it('value is array', () => {
const startDate = ArrayValueObject.create({
calculateValueList: transformToValueObject([
@@ -87,5 +98,24 @@ describe('Test eomonth function', () => {
const result = testFunction.calculate(startDate, months);
expect(result.getValue()).toStrictEqual(ErrorType.VALUE);
});
it('checks the serial-zero boundary in both date systems', () => {
const startDate = NumberValueObject.create(0);
const months = NumberValueObject.create(0);
testFunction.setDateSystem(DateSystem.Date1900);
expect(testFunction.calculate(startDate, months).getValue()).toBe(31);
testFunction.setDateSystem(DateSystem.Date1904);
expect(testFunction.calculate(startDate, months).getValue()).toBe(30);
testFunction.setDateSystem(DateSystem.Date1900);
});
it('keeps the fictitious 1900-02-29 serial when it is the month end', () => {
testFunction.setDateSystem(DateSystem.Date1900);
expect(testFunction.calculate(NumberValueObject.create(60), NumberValueObject.create(0)).getValue()).toBe(59);
});
});
});
@@ -16,7 +16,8 @@
import type { ArrayValueObject } from '../../../engine/value-object/array-value-object';
import type { BaseValueObject } from '../../../engine/value-object/base-value-object';
import { excelDateSerial, excelSerialToDate, getDateSerialNumberByObject } from '../../../basics/date';
import { DateSystem, excelDateSerial, excelSerialToDate } from '@univerjs/core';
import { getDateSerialNumberByObject } from '../../../basics/date';
import { ErrorType } from '../../../basics/error-type';
import { ErrorValueObject } from '../../../engine/value-object/base-value-object';
import { NumberValueObject } from '../../../engine/value-object/primitive-object';
@@ -61,7 +62,7 @@ export class Eomonth extends BaseFunction {
return _months;
}
const startDateSerialNumber = getDateSerialNumberByObject(_startDate);
const startDateSerialNumber = getDateSerialNumberByObject(_startDate, this.getDateSystem());
if (typeof startDateSerialNumber !== 'number') {
return startDateSerialNumber;
@@ -71,9 +72,10 @@ export class Eomonth extends BaseFunction {
return ErrorValueObject.create(ErrorType.VALUE);
}
const startDateDate = excelSerialToDate(startDateSerialNumber);
const startYear = startDateSerialNumber > 0 ? startDateDate.getUTCFullYear() : 1900;
const startMonth = startDateSerialNumber > 0 ? startDateDate.getUTCMonth() : 0;
const startDateDate = excelSerialToDate(startDateSerialNumber, this.getDateSystem());
const isDate1900SerialZero = this.getDateSystem() === DateSystem.Date1900 && startDateSerialNumber === 0;
const startYear = isDate1900SerialZero ? 1900 : startDateDate.getUTCFullYear();
const startMonth = isDate1900SerialZero ? 0 : startDateDate.getUTCMonth();
const monthsValue = Math.floor(+_months.getValue());
@@ -82,7 +84,7 @@ export class Eomonth extends BaseFunction {
}
const targetDate = new Date(Date.UTC(startYear, startMonth + monthsValue + 1, 0));
const result = excelDateSerial(targetDate);
const result = excelDateSerial(targetDate, this.getDateSystem());
return NumberValueObject.create(result);
}
@@ -14,6 +14,7 @@
* limitations under the License.
*/
import { DateSystem } from '@univerjs/core';
import { describe, expect, it } from 'vitest';
import { ErrorType } from '../../../../basics/error-type';
import { ArrayValueObject, transformToValueObject } from '../../../../engine/value-object/array-value-object';
@@ -33,6 +34,18 @@ describe('Test epochtodate function', () => {
expect(getObjectValue(result)).toBe(44734.58692129629);
});
it('uses the configured date system for Unix epoch', () => {
const timestamp = NumberValueObject.create(0);
testFunction.setDateSystem(DateSystem.Date1900);
expect(getObjectValue(testFunction.calculate(timestamp))).toBe(25569);
testFunction.setDateSystem(DateSystem.Date1904);
expect(getObjectValue(testFunction.calculate(timestamp))).toBe(24107);
testFunction.setDateSystem(DateSystem.Date1900);
});
it('Timestamp value test', () => {
const timestamp = NullValueObject.create();
const result = testFunction.calculate(timestamp);
@@ -17,7 +17,7 @@
import type { BaseReferenceObject, FunctionVariantType } from '../../../engine/reference-object/base-reference-object';
import type { ArrayValueObject } from '../../../engine/value-object/array-value-object';
import type { BaseValueObject } from '../../../engine/value-object/base-value-object';
import { excelDateTimeSerial } from '../../../basics/date';
import { excelDateTimeSerial } from '@univerjs/core';
import { ErrorType } from '../../../basics/error-type';
import { checkVariantsErrorIsStringToNumber } from '../../../engine/utils/check-variant-error';
import { ErrorValueObject } from '../../../engine/value-object/base-value-object';
@@ -75,11 +75,11 @@ export class Epochtodate extends BaseFunction {
const date = new Date(Date.UTC(1970, 0, 1, 0, 0, 0, 0) + timestampValue);
if (!Number.isNaN(date.getTime())) {
const dateSerialNumber = excelDateTimeSerial(date);
const dateSerialNumber = excelDateTimeSerial(date, this.getDateSystem());
return NumberValueObject.create(dateSerialNumber, 'yyyy-MM-dd AM/PM hh:mm:ss');
} else {
const result = 25569 + timestampValue / 86400000;
const result = excelDateTimeSerial(new Date(0), this.getDateSystem()) + timestampValue / 86400000;
return NumberValueObject.create(result);
}
@@ -32,6 +32,10 @@ describe('Test hour function', () => {
expect(result.getValue()).toStrictEqual(5);
});
it('keeps the time portion when the date portion is serial 60', () => {
expect(testFunction.calculate(NumberValueObject.create(60.5)).getValue()).toBe(12);
});
it('Serial number is error', () => {
const serialNumber = ErrorValueObject.create(ErrorType.NAME);
const result = testFunction.calculate(serialNumber);
@@ -15,7 +15,8 @@
*/
import type { BaseValueObject } from '../../../engine/value-object/base-value-object';
import { excelSerialToDateTime, getDateSerialNumberByObject } from '../../../basics/date';
import { excelSerialToDateTime } from '@univerjs/core';
import { getDateSerialNumberByObject } from '../../../basics/date';
import { NumberValueObject } from '../../../engine/value-object/primitive-object';
import { BaseFunction } from '../../base-function';
@@ -43,7 +44,7 @@ export class Hour extends BaseFunction {
}
private _handleSingleObject(serialNumberObject: BaseValueObject) {
const dateSerialNumber = getDateSerialNumberByObject(serialNumberObject);
const dateSerialNumber = getDateSerialNumberByObject(serialNumberObject, this.getDateSystem());
if (typeof dateSerialNumber !== 'number') {
return dateSerialNumber;
@@ -53,7 +54,7 @@ export class Hour extends BaseFunction {
return NumberValueObject.create(0);
}
const date = excelSerialToDateTime(dateSerialNumber);
const date = excelSerialToDateTime(dateSerialNumber, this.getDateSystem());
const hours = date.getUTCHours();
return NumberValueObject.create(hours);
@@ -14,6 +14,7 @@
* limitations under the License.
*/
import { DateSystem } from '@univerjs/core';
import { describe, expect, it } from 'vitest';
import { ErrorType } from '../../../../basics/error-type';
import { ArrayValueObject, transformToValue, transformToValueObject } from '../../../../engine/value-object/array-value-object';
@@ -42,6 +43,14 @@ describe('Test isoweeknum function', () => {
expect(result.getValue()).toStrictEqual(52);
});
it('uses the 1904 epoch when serial is zero', () => {
testFunction.setDateSystem(DateSystem.Date1900);
expect(testFunction.calculate(NumberValueObject.create(0)).getValue()).toBe(52);
testFunction.setDateSystem(DateSystem.Date1904);
expect(testFunction.calculate(NumberValueObject.create(0)).getValue()).toBe(53);
testFunction.setDateSystem(DateSystem.Date1900);
});
it('date is error', () => {
const date = ErrorValueObject.create(ErrorType.NAME);
const result = testFunction.calculate(date);
@@ -77,5 +86,22 @@ describe('Test isoweeknum function', () => {
[52, 52, 52, 52, ErrorType.NAME, 4, 8],
]);
});
it('checks the serial-zero boundary in both date systems', () => {
const date = NumberValueObject.create(0);
testFunction.setDateSystem(DateSystem.Date1900);
expect(testFunction.calculate(date).getValue()).toBe(52);
testFunction.setDateSystem(DateSystem.Date1904);
expect(testFunction.calculate(date).getValue()).toBe(53);
testFunction.setDateSystem(DateSystem.Date1900);
});
it('uses the Excel weekday for serial 60 in the 1900 date system', () => {
testFunction.setDateSystem(DateSystem.Date1900);
expect(testFunction.calculate(NumberValueObject.create(60)).getValue()).toBe(9);
});
});
});
@@ -15,7 +15,8 @@
*/
import type { BaseValueObject } from '../../../engine/value-object/base-value-object';
import { excelDateSerial, excelSerialToDate, getDateSerialNumberByObject, getWeekDayByDateSerialNumber } from '../../../basics/date';
import { DateSystem, excelDateSerial, excelSerialToDate } from '@univerjs/core';
import { getDateSerialNumberByObject, getWeekDayByDateSerialNumber } from '../../../basics/date';
import { NumberValueObject } from '../../../engine/value-object/primitive-object';
import { BaseFunction } from '../../base-function';
@@ -43,17 +44,19 @@ export class Isoweeknum extends BaseFunction {
}
private _handleSingleObject(date: BaseValueObject) {
const dateSerialNumber = getDateSerialNumberByObject(date);
const dateSerialNumber = getDateSerialNumberByObject(date, this.getDateSystem());
if (typeof dateSerialNumber !== 'number') {
return dateSerialNumber;
}
const currentDate = excelSerialToDate(dateSerialNumber);
const currentYear = dateSerialNumber > 0 ? currentDate.getUTCFullYear() : 1900;
const currentDate = excelSerialToDate(dateSerialNumber, this.getDateSystem());
const currentYear = this.getDateSystem() === DateSystem.Date1900 && dateSerialNumber === 0
? 1900
: currentDate.getUTCFullYear();
let yearStart = new Date(Date.UTC(currentYear, 0, 1));
let yearStartSerialNumber = excelDateSerial(yearStart);
let yearStartWeekDay = getWeekDayByDateSerialNumber(yearStartSerialNumber);
let yearStartSerialNumber = excelDateSerial(yearStart, this.getDateSystem());
let yearStartWeekDay = getWeekDayByDateSerialNumber(yearStartSerialNumber, this.getDateSystem());
let yearWeekStartSerialNumber: number;
@@ -67,8 +70,8 @@ export class Isoweeknum extends BaseFunction {
if (dateSerialNumber < yearWeekStartSerialNumber) {
yearStart = new Date(Date.UTC(currentYear - 1, 0, 1));
yearStartSerialNumber = excelDateSerial(yearStart);
yearStartWeekDay = getWeekDayByDateSerialNumber(yearStartSerialNumber);
yearStartSerialNumber = excelDateSerial(yearStart, this.getDateSystem());
yearStartWeekDay = getWeekDayByDateSerialNumber(yearStartSerialNumber, this.getDateSystem());
if (yearStartWeekDay < 1) {
yearWeekStartSerialNumber = yearStartSerialNumber + 1;
@@ -32,6 +32,10 @@ describe('Test minute function', () => {
expect(result.getValue()).toStrictEqual(35);
});
it('keeps the time portion when the date portion is serial 60', () => {
expect(testFunction.calculate(NumberValueObject.create(60.5)).getValue()).toBe(0);
});
it('Serial number is error', () => {
const serialNumber = ErrorValueObject.create(ErrorType.NAME);
const result = testFunction.calculate(serialNumber);
@@ -15,7 +15,8 @@
*/
import type { BaseValueObject } from '../../../engine/value-object/base-value-object';
import { excelSerialToDateTime, getDateSerialNumberByObject } from '../../../basics/date';
import { excelSerialToDateTime } from '@univerjs/core';
import { getDateSerialNumberByObject } from '../../../basics/date';
import { NumberValueObject } from '../../../engine/value-object/primitive-object';
import { BaseFunction } from '../../base-function';
@@ -43,7 +44,7 @@ export class Minute extends BaseFunction {
}
private _handleSingleObject(serialNumberObject: BaseValueObject) {
const dateSerialNumber = getDateSerialNumberByObject(serialNumberObject);
const dateSerialNumber = getDateSerialNumberByObject(serialNumberObject, this.getDateSystem());
if (typeof dateSerialNumber !== 'number') {
return dateSerialNumber;
@@ -53,7 +54,7 @@ export class Minute extends BaseFunction {
return NumberValueObject.create(0);
}
const date = excelSerialToDateTime(dateSerialNumber);
const date = excelSerialToDateTime(dateSerialNumber, this.getDateSystem());
const minutes = date.getUTCMinutes();
return NumberValueObject.create(minutes);
@@ -14,6 +14,7 @@
* limitations under the License.
*/
import { DateSystem } from '@univerjs/core';
import { describe, expect, it } from 'vitest';
import { ArrayValueObject, transformToValue, transformToValueObject } from '../../../../engine/value-object/array-value-object';
import { NumberValueObject, StringValueObject } from '../../../../engine/value-object/primitive-object';
@@ -30,6 +31,18 @@ describe('Test month function', () => {
expect(result.getValue()).toStrictEqual(1);
});
it('uses the configured date system for serials around 1904', () => {
const serialNumber = NumberValueObject.create(31);
testFunction.setDateSystem(DateSystem.Date1900);
expect(testFunction.calculate(serialNumber).getValue()).toBe(1);
testFunction.setDateSystem(DateSystem.Date1904);
expect(testFunction.calculate(serialNumber).getValue()).toBe(2);
testFunction.setDateSystem(DateSystem.Date1900);
});
it('Serial number is date string', () => {
const serialNumber = StringValueObject.create('2020-01-02');
const result = testFunction.calculate(serialNumber);
@@ -15,7 +15,8 @@
*/
import type { BaseValueObject } from '../../../engine/value-object/base-value-object';
import { excelSerialToDate, isValidDateStr } from '../../../basics/date';
import { excelSerialToDateTimeParts } from '@univerjs/core';
import { isValidDateStr } from '../../../basics/date';
import { ErrorType } from '../../../basics/error-type';
import { ErrorValueObject } from '../../../engine/value-object/base-value-object';
import { NumberValueObject } from '../../../engine/value-object/primitive-object';
@@ -55,13 +56,8 @@ export class Month extends BaseFunction {
return ErrorValueObject.create(ErrorType.NUM);
}
// Excel serial 0 is 1900-01-00
// Google Sheets serial 0 is 1899-12-30
if (dateSerial === 0) {
return NumberValueObject.create(1);
}
date = excelSerialToDate(dateSerial);
const parts = excelSerialToDateTimeParts(dateSerial, { dateSystem: this.getDateSystem() });
return parts ? NumberValueObject.create(parts.month) : ErrorValueObject.create(ErrorType.NUM);
}
const month = date.getUTCMonth() + 1;
@@ -14,6 +14,7 @@
* limitations under the License.
*/
import { DateSystem } from '@univerjs/core';
import { describe, expect, it } from 'vitest';
import { ErrorType } from '../../../../basics/error-type';
import { ArrayValueObject, transformToValueObject } from '../../../../engine/value-object/array-value-object';
@@ -24,7 +25,7 @@ import { FUNCTION_NAMES_DATE } from '../../function-names';
import { NetworkdaysIntl } from '../index';
describe('Test networkdays.intl function', () => {
const testFunction = new NetworkdaysIntl(FUNCTION_NAMES_DATE.NETWORKDAYS);
const testFunction = new NetworkdaysIntl(FUNCTION_NAMES_DATE.NETWORKDAYS_INTL);
describe('NetworkdaysIntl', () => {
it('Value is all date string', () => {
@@ -56,6 +57,19 @@ describe('Test networkdays.intl function', () => {
expect(getObjectValue(result3)).toStrictEqual(85);
});
it('uses the configured date system for serial zero', () => {
const startDate = NumberValueObject.create(0);
const endDate = NumberValueObject.create(0);
testFunction.setDateSystem(DateSystem.Date1900);
expect(getObjectValue(testFunction.calculate(startDate, endDate))).toBe(0);
testFunction.setDateSystem(DateSystem.Date1904);
expect(getObjectValue(testFunction.calculate(startDate, endDate))).toBe(1);
testFunction.setDateSystem(DateSystem.Date1900);
});
it('Value is number or date string', () => {
const startDate = NumberValueObject.create(45122);
const endDate = StringValueObject.create('2013-3-1');
@@ -218,5 +232,18 @@ describe('Test networkdays.intl function', () => {
const result3 = testFunction.calculate(startDate3, endDate3);
expect(getObjectValue(result3)).toBe(2);
});
it('checks the serial-zero boundary in both date systems', () => {
const startDate = NumberValueObject.create(0);
const endDate = NumberValueObject.create(366);
const weekend = NumberValueObject.create(1);
testFunction.setDateSystem(DateSystem.Date1900);
expect(getObjectValue(testFunction.calculate(startDate, endDate, weekend))).toBe(261);
testFunction.setDateSystem(DateSystem.Date1904);
expect(getObjectValue(testFunction.calculate(startDate, endDate, weekend))).toBe(261);
testFunction.setDateSystem(DateSystem.Date1900);
});
});
});
@@ -107,13 +107,13 @@ export class NetworkdaysIntl extends BaseFunction {
return ErrorValueObject.create(ErrorType.VALUE);
}
const startDateSerialNumber = getDateSerialNumberByObject(_startDate);
const startDateSerialNumber = getDateSerialNumberByObject(_startDate, this.getDateSystem());
if (typeof startDateSerialNumber !== 'number') {
return startDateSerialNumber;
}
const endDateSerialNumber = getDateSerialNumberByObject(_endDate);
const endDateSerialNumber = getDateSerialNumberByObject(_endDate, this.getDateSystem());
if (typeof endDateSerialNumber !== 'number') {
return endDateSerialNumber;
@@ -123,7 +123,7 @@ export class NetworkdaysIntl extends BaseFunction {
return this._getResultByHolidays(startDateSerialNumber, endDateSerialNumber, weekendValue, holidays);
}
const result = countWorkingDays(startDateSerialNumber, endDateSerialNumber, weekendValue);
const result = countWorkingDays(startDateSerialNumber, endDateSerialNumber, weekendValue, undefined, this.getDateSystem());
return NumberValueObject.create(result);
}
@@ -147,7 +147,7 @@ export class NetworkdaysIntl extends BaseFunction {
continue;
}
const holidaySerialNumber = getDateSerialNumberByObject(cell);
const holidaySerialNumber = getDateSerialNumberByObject(cell, this.getDateSystem());
if (typeof holidaySerialNumber !== 'number') {
if (cell.isString()) {
@@ -164,7 +164,7 @@ export class NetworkdaysIntl extends BaseFunction {
return ErrorValueObject.create(ErrorType.VALUE);
}
const holidaySerialNumber = getDateSerialNumberByObject(holidays);
const holidaySerialNumber = getDateSerialNumberByObject(holidays, this.getDateSystem());
if (typeof holidaySerialNumber !== 'number') {
return holidaySerialNumber;
@@ -173,7 +173,7 @@ export class NetworkdaysIntl extends BaseFunction {
holidaysValueArray.push(holidaySerialNumber);
}
const result = countWorkingDays(startDateSerialNumber, endDateSerialNumber, weekendValue, holidaysValueArray);
const result = countWorkingDays(startDateSerialNumber, endDateSerialNumber, weekendValue, holidaysValueArray, this.getDateSystem());
return NumberValueObject.create(result);
}
@@ -14,6 +14,7 @@
* limitations under the License.
*/
import { DateSystem } from '@univerjs/core';
import { describe, expect, it } from 'vitest';
import { ErrorType } from '../../../../basics/error-type';
import { ArrayValueObject, transformToValueObject } from '../../../../engine/value-object/array-value-object';
@@ -53,6 +54,19 @@ describe('Test networkdays function', () => {
expect(result3.getValue()).toStrictEqual(107);
});
it('uses the configured date system for serial zero', () => {
const startDate = NumberValueObject.create(0);
const endDate = NumberValueObject.create(0);
testFunction.setDateSystem(DateSystem.Date1900);
expect(testFunction.calculate(startDate, endDate).getValue()).toBe(0);
testFunction.setDateSystem(DateSystem.Date1904);
expect(testFunction.calculate(startDate, endDate).getValue()).toBe(1);
testFunction.setDateSystem(DateSystem.Date1900);
});
it('Value is number or date string', () => {
const startDate = NumberValueObject.create(45122);
const endDate = StringValueObject.create('2013-3-1');
@@ -135,5 +149,17 @@ describe('Test networkdays function', () => {
const result3 = testFunction.calculate(startDate3, endDate3, holidays3);
expect(result3.getValue()).toStrictEqual(ErrorType.NAME);
});
it('checks the serial-zero boundary in both date systems', () => {
const startDate = NumberValueObject.create(0);
const endDate = NumberValueObject.create(366);
testFunction.setDateSystem(DateSystem.Date1900);
expect(testFunction.calculate(startDate, endDate).getValue()).toBe(261);
testFunction.setDateSystem(DateSystem.Date1904);
expect(testFunction.calculate(startDate, endDate).getValue()).toBe(261);
testFunction.setDateSystem(DateSystem.Date1900);
});
});
});
@@ -69,13 +69,13 @@ export class Networkdays extends BaseFunction {
return ErrorValueObject.create(ErrorType.VALUE);
}
const startDateSerialNumber = getDateSerialNumberByObject(_startDate);
const startDateSerialNumber = getDateSerialNumberByObject(_startDate, this.getDateSystem());
if (typeof startDateSerialNumber !== 'number') {
return startDateSerialNumber;
}
const endDateSerialNumber = getDateSerialNumberByObject(_endDate);
const endDateSerialNumber = getDateSerialNumberByObject(_endDate, this.getDateSystem());
if (typeof endDateSerialNumber !== 'number') {
return endDateSerialNumber;
@@ -85,7 +85,7 @@ export class Networkdays extends BaseFunction {
return this._getResultByHolidays(startDateSerialNumber, endDateSerialNumber, holidays);
}
const result = countWorkingDays(startDateSerialNumber, endDateSerialNumber);
const result = countWorkingDays(startDateSerialNumber, endDateSerialNumber, 1, undefined, this.getDateSystem());
return NumberValueObject.create(result);
}
@@ -109,7 +109,7 @@ export class Networkdays extends BaseFunction {
continue;
}
const holidaySerialNumber = getDateSerialNumberByObject(cell);
const holidaySerialNumber = getDateSerialNumberByObject(cell, this.getDateSystem());
if (typeof holidaySerialNumber !== 'number') {
if (cell.isString()) {
@@ -126,7 +126,7 @@ export class Networkdays extends BaseFunction {
return ErrorValueObject.create(ErrorType.VALUE);
}
const holidaySerialNumber = getDateSerialNumberByObject(holidays);
const holidaySerialNumber = getDateSerialNumberByObject(holidays, this.getDateSystem());
if (typeof holidaySerialNumber !== 'number') {
return holidaySerialNumber;
@@ -135,7 +135,7 @@ export class Networkdays extends BaseFunction {
holidaysValueArray.push(holidaySerialNumber);
}
const result = countWorkingDays(startDateSerialNumber, endDateSerialNumber, 1, holidaysValueArray);
const result = countWorkingDays(startDateSerialNumber, endDateSerialNumber, 1, holidaysValueArray, this.getDateSystem());
return NumberValueObject.create(result);
}

Some files were not shown because too many files have changed in this diff Show More