From b883754138502edf84789f6dc1f966079d8d8ad2 Mon Sep 17 00:00:00 2001 From: righ Date: Sun, 1 Dec 2024 04:33:56 +0900 Subject: [PATCH] fix: some bugs --- .github/workflows/check.yaml | 2 +- .github/workflows/e2e.yaml | 4 + .github/workflows/unittest.yaml | 4 + .storybook/examples/basic/size.stories.tsx | 13 ++ e2e/time.spec.ts | 30 ++++ package.json | 3 +- src/constants.ts | 3 + src/formula/functions/__utils.ts | 78 ++++++--- src/formula/functions/add.ts | 12 +- src/formula/functions/countif.spec.ts | 183 +++++++++++++++++++++ src/formula/functions/countif.ts | 3 +- src/formula/functions/eq.spec.ts | 91 ++++++++++ src/formula/functions/eq.ts | 5 +- src/formula/functions/gt.ts | 5 +- src/formula/functions/gte.ts | 5 +- src/formula/functions/lt.ts | 5 +- src/formula/functions/lte.ts | 5 +- src/formula/functions/minus.ts | 7 +- src/formula/functions/ne.ts | 3 +- src/formula/functions/sumif.ts | 3 +- src/lib/autofill.ts | 4 +- src/lib/time.ts | 28 +--- src/parsers/core.ts | 23 ++- src/renderers/core.ts | 13 +- yarn.lock | 33 +--- 25 files changed, 458 insertions(+), 107 deletions(-) create mode 100644 e2e/time.spec.ts create mode 100644 src/formula/functions/countif.spec.ts create mode 100644 src/formula/functions/eq.spec.ts diff --git a/.github/workflows/check.yaml b/.github/workflows/check.yaml index 3c17114a..0ea22e68 100644 --- a/.github/workflows/check.yaml +++ b/.github/workflows/check.yaml @@ -1,7 +1,7 @@ name: typecheck on: push: - + workflow_dispatch: jobs: type-check: diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml index 06eade51..0f74b58a 100644 --- a/.github/workflows/e2e.yaml +++ b/.github/workflows/e2e.yaml @@ -1,6 +1,10 @@ name: e2e on: push: + workflow_dispatch: + +env: + TZ: 'Asia/Tokyo' jobs: run-e2e-tests: diff --git a/.github/workflows/unittest.yaml b/.github/workflows/unittest.yaml index d7499901..d6dafa86 100644 --- a/.github/workflows/unittest.yaml +++ b/.github/workflows/unittest.yaml @@ -1,6 +1,10 @@ name: unittest on: push: + workflow_dispatch: + +env: + TZ: 'Asia/Tokyo' jobs: run-unit-tests: diff --git a/.storybook/examples/basic/size.stories.tsx b/.storybook/examples/basic/size.stories.tsx index 78682b36..b4d90096 100644 --- a/.storybook/examples/basic/size.stories.tsx +++ b/.storybook/examples/basic/size.stories.tsx @@ -1,6 +1,7 @@ import React from "react"; import { ComponentStory } from "@storybook/react"; import { CellsByAddressType, constructInitialCells, GridSheet } from "../../../src"; +import { TimeDelta } from "../../../src/lib/time"; export default { title: "Basic", @@ -42,6 +43,18 @@ const Sheet = ({ numRows, numCols, defaultWidth, initialCells }: Props) => { C3: { value: 3, }, + A4: { + value: new Date("2022-03-05T12:34:56+09:00") + }, + B4: { + value: TimeDelta.create(11, 11, 11), + }, + C4: { + value: "=A4+B4", + }, + A5: { + value: "=A4-13/24", + }, ...initialCells, }, ensured: { numRows, numCols }, diff --git a/e2e/time.spec.ts b/e2e/time.spec.ts new file mode 100644 index 00000000..0d1c100f --- /dev/null +++ b/e2e/time.spec.ts @@ -0,0 +1,30 @@ +import { test, expect } from '@playwright/test'; + +test('time + delta, time + number(days)', async ({ page }) => { + await page.goto('http://localhost:5233/iframe.html?id=basic--small&viewMode=story'); + const a4 = page.locator("[data-address='A4']"); + const b4 = page.locator("[data-address='B4']"); + const c4 = page.locator("[data-address='C4']"); + const a5 = page.locator("[data-address='A5']"); + + expect(await a4.locator('.gs-cell-rendered').textContent()).toBe('2022-03-05 12:34:56'); + expect(await b4.locator('.gs-cell-rendered').textContent()).toBe('11:11:11'); + expect(await c4.locator('.gs-cell-rendered').textContent()).toBe('2022-03-05 23:46:07'); + expect(await a5.locator('.gs-cell-rendered').textContent()).toBe('2022-03-04 23:34:56'); +}); + +test('input DD MMM [YYYY] format', async ({ page }) => { + await page.goto('http://localhost:5233/iframe.html?id=basic--small&viewMode=story'); + + const b5 = page.locator("[data-address='B5']"); + await b5.click(); + await page.keyboard.type('30 Nov'); + await page.keyboard.press('Enter'); + expect(await b5.locator('.gs-cell-rendered').textContent()).toBe('2001-11-29 15:00:00'); + + const c5 = page.locator("[data-address='C5']"); + await c5.click(); + await page.keyboard.type('30 Nov 2024'); + await page.keyboard.press('Enter'); + expect(await c5.locator('.gs-cell-rendered').textContent()).toBe('2024-11-29 15:00:00'); +}); diff --git a/package.json b/package.json index f5883dcd..3445b597 100644 --- a/package.json +++ b/package.json @@ -83,8 +83,7 @@ "react-dom": ">=16.9.0" }, "dependencies": { - "date-fns": "^2.28.0", - "date-fns-timezone": "^0.1.4" + "dayjs": "^1.11.13" }, "resolutions": { "trim": "^0.0.3", diff --git a/src/constants.ts b/src/constants.ts index faaaa3a0..30e9535d 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -31,3 +31,6 @@ export class Special { this.name = name; } } + +export const SECONDS_IN_DAY = 86400; +export const FULLDATE_FORMAT_UTC = 'YYYY-MM-DDTHH:mm:ss.SSSZ'; diff --git a/src/formula/functions/__utils.ts b/src/formula/functions/__utils.ts index 03dece34..e8367ee0 100644 --- a/src/formula/functions/__utils.ts +++ b/src/formula/functions/__utils.ts @@ -1,6 +1,46 @@ import { solveTable } from '../solver'; import { Table } from '../../lib/table'; import { FormulaError } from '../evaluator'; +import dayjs from 'dayjs'; +import { FULLDATE_FORMAT_UTC } from '../../constants'; + +export const gt = (left: any, right: any): boolean => { + if (typeof left === 'string' || typeof right === 'string') { + return ensureString(left) > ensureString(right); + } + try { + return ensureNumber(left) > ensureNumber(right); + } catch { + return false; + } +}; + +export const gte = (left: any, right: any): boolean => { + if (typeof left === 'string' || typeof right === 'string') { + return ensureString(left) >= ensureString(right); + } + try { + return ensureNumber(left) >= ensureNumber(right); + } catch { + return false; + } +}; + +export const lt = (left: any, right: any): boolean => { + return !gte(left, right); +}; + +export const lte = (left: any, right: any): boolean => { + return !gt(left, right); +}; + +export const eq = (left: any, right: any): boolean => { + return ensureString(left) === ensureString(right); +}; + +export const ne = (left: any, right: any): boolean => { + return !eq(left, right); +} export const ensureNumber = (value: any, alternative?: number): number => { if (typeof value === 'undefined' && typeof alternative !== 'undefined') { @@ -14,6 +54,9 @@ export const ensureNumber = (value: any, alternative?: number): number => { const v = stripTable(value, 0, 0); return ensureNumber(v, alternative); } + if (value instanceof Date) { + return value.getTime(); + } const num = parseFloat(value as string); if (isNaN(num)) { throw new FormulaError('#VALUE!', `${value} cannot be converted to a number`); @@ -35,10 +78,7 @@ export const ensureString = (value: any): string => { switch (value.constructor.name) { case 'Date': { const d: Date = value; - if (d.getHours() + d.getMinutes() + d.getSeconds() === 0) { - return d.toLocaleDateString(); - } - return d.toLocaleString(); + return dayjs(d).format(FULLDATE_FORMAT_UTC); } default: return String(value); @@ -77,44 +117,44 @@ export const stripTable = (value: any, y = 0, x = 0) => { return value; }; -const CONDITION_REGEX = /^(?|<=|>=|<>|>|<|=)?(?.*)$/; +const CONDITION_REGEX = /^(<=|>=|<>|>|<|=)?(.*)$/; -export const check = (value: any, condition: string) => { +export const check = (value: any, condition: string): boolean => { const m = condition.match(CONDITION_REGEX); // eslint-disable-next-line no-unsafe-optional-chaining // eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain - const { expr = '', target = '' } = m?.groups || {}; - - const comparison = parseFloat(target); + const [, expr = '', target = ''] = m || []; + let comparison: any = target; if (expr === '>' || expr === '<' || expr === '>=' || expr === '<=') { - if (isNaN(comparison) === (typeof value === 'number')) { - return false; + if (typeof value === 'number') { + comparison = parseFloat(target); } switch (expr) { case '>': - return value > target; + return gt(value, comparison); case '>=': - return value >= target; + return gte(value, comparison); case '<': - return value < target; + return lt(value, comparison); case '<=': - return value <= target; + return lte(value, comparison); } } const equals = expr === '' || expr === '='; if (target === '') { - return !value === equals; + // empty target means "" or "<>" + return (value == null || value === '') === equals; } - if (isNaN(comparison) && (typeof value === 'string' || value instanceof String)) { + if (typeof value === 'string' || value instanceof String) { const replaced = target .replace(/~\*/g, '(\\*)') .replace(/~\?/g, '(\\?)') .replace(/\*/g, '(.*)') - .replace(/\?/g, '(.?)'); + .replace(/\?/g, '(.)'); const regex = RegExp(`^${replaced}$`, 'i'); return regex.test(value as string) === equals; } - return (value == comparison) === equals; + return eq(value, comparison) === equals; }; diff --git a/src/formula/functions/add.ts b/src/formula/functions/add.ts index d9f5c316..ce42a106 100644 --- a/src/formula/functions/add.ts +++ b/src/formula/functions/add.ts @@ -1,9 +1,11 @@ +import dayjs from 'dayjs'; + import { FormulaError } from '../evaluator'; import { BaseFunction } from './__base'; import { ensureNumber, stripTable } from './__utils'; import { Table } from '../../lib/table'; import { TimeDelta } from '../../lib/time'; -import { addSeconds } from 'date-fns'; +import { SECONDS_IN_DAY } from '../../constants'; export class AddFunction extends BaseFunction { example = 'ADD(2, 3)'; @@ -36,10 +38,14 @@ export class AddFunction extends BaseFunction { return v1.add(v2); } if (v1 instanceof Date && typeof v2 === 'number') { - return addSeconds(v1, v2); + return dayjs(v1) + .add(v2 * SECONDS_IN_DAY, 'second') + .toDate(); } if (typeof v1 === 'number' && v2 instanceof Date) { - return addSeconds(v2, v1); + return dayjs(v2) + .add(v1 * SECONDS_IN_DAY, 'second') + .toDate(); } if (!v1) { return v2; diff --git a/src/formula/functions/countif.spec.ts b/src/formula/functions/countif.spec.ts new file mode 100644 index 00000000..8a7a6ccb --- /dev/null +++ b/src/formula/functions/countif.spec.ts @@ -0,0 +1,183 @@ +import { CountifFunction } from './countif'; +import { Table } from '../../lib/table'; +import { FormulaError, RangeEntity, RefEntity, ValueEntity } from '../evaluator'; + +describe('countif', () => { + const table = new Table({}); + table.initialize({ + A1: { value: 1 }, + B1: { value: 5 }, + C1: { value: 3 }, + D1: { value: 2 }, + E1: { value: 3 }, + A2: { value: 'a' }, + B2: { value: 'aa' }, + C2: { value: 'a日' }, + D2: { value: 'abc' }, + E2: { value: 'bb' }, + A3: { value: new Date('2022-01-05T00:00:00.000Z') }, + B3: { value: new Date('2022-01-03T00:00:00.000Z') }, + C3: { value: new Date('2022-01-04T00:00:00.000Z') }, + D3: { value: new Date('2022-01-04T00:00:00.000+09:00') }, + A4: { value: null }, + B4: { value: 0 }, + C4: { value: '' }, + D4: { value: false }, + E4: { value: true }, + }); + describe('normal', () => { + it('eq', () => { + { + const f = new CountifFunction({ + table, + args: [new RangeEntity('A1:E1'), new ValueEntity('3')], + }); + expect(f.call()).toBe(2); + } + { + const f = new CountifFunction({ + table, + args: [new RangeEntity('A1:E1'), new ValueEntity('=3')], + }); + expect(f.call()).toBe(2); + } + { + const f = new CountifFunction({ + table, + args: [new RangeEntity('A1:E1'), new RefEntity('E1')], + }); + expect(f.call()).toBe(2); + } + { + const f = new CountifFunction({ + table, + args: [new RangeEntity('A2:E2'), new ValueEntity('a*')], + }); + expect(f.call()).toBe(4); + } + { + const f = new CountifFunction({ + table, + args: [new RangeEntity('A2:E2'), new ValueEntity('a?')], + }); + expect(f.call()).toBe(2); + } + { + const f = new CountifFunction({ + table, + args: [new RangeEntity('A2:E2'), new ValueEntity('?b*')], + }); + expect(f.call()).toBe(2); + } + { + const f = new CountifFunction({ + table, + args: [new RangeEntity('A3:E3'), new ValueEntity(new Date('2022-01-04T00:00:00.000Z'))], + }); + expect(f.call()).toBe(1); + } + { + const f = new CountifFunction({ + table, + args: [new RangeEntity('A4:E4'), new ValueEntity('')], + }); + expect(f.call()).toBe(2); + } + }); + it('ne', () => { + { + const f = new CountifFunction({ + table, + args: [new RangeEntity('A1:E1'), new ValueEntity('<>3')], + }); + expect(f.call()).toBe(3); + } + { + const f = new CountifFunction({ + table, + args: [new RangeEntity('A2:E2'), new ValueEntity('<>a*')], + }); + expect(f.call()).toBe(1); + } + { + const f = new CountifFunction({ + table, + args: [new RangeEntity('A4:E4'), new ValueEntity('<>')], + }); + expect(f.call()).toBe(3); + } + }); + it('gt', () => { + { + const f = new CountifFunction({ + table, + args: [new RangeEntity('A1:E1'), new ValueEntity('>3')], + }); + expect(f.call()).toBe(1); + } + { + const f = new CountifFunction({ + table, + args: [new RangeEntity('A2:E2'), new ValueEntity('>aa')], + }); + expect(f.call()).toBe(3); + } + }); + it('gte', () => { + { + const f = new CountifFunction({ + table, + args: [new RangeEntity('A1:E1'), new ValueEntity('>=3')], + }); + expect(f.call()).toBe(3); + } + { + const f = new CountifFunction({ + table, + args: [new RangeEntity('A2:E2'), new ValueEntity('>=aa')], + }); + expect(f.call()).toBe(4); + } + }); + it('lt', () => { + { + const f = new CountifFunction({ + table, + args: [new RangeEntity('A1:E1'), new ValueEntity('<3')], + }); + expect(f.call()).toBe(2); + } + { + const f = new CountifFunction({ + table, + args: [new RangeEntity('A2:E2'), new ValueEntity(' { + { + const f = new CountifFunction({ + table, + args: [new RangeEntity('A1:E1'), new ValueEntity('<=3')], + }); + expect(f.call()).toBe(4); + } + { + const f = new CountifFunction({ + table, + args: [new RangeEntity('A2:E2'), new ValueEntity('<=aa')], + }); + expect(f.call()).toBe(2); + } + }); + }); + describe('validation error', () => { + it('missing argument', () => { + { + const f = new CountifFunction({ table, args: [new RangeEntity('A1:A3')] }); + expect(f.call.bind(f)).toThrow(FormulaError); + } + }); + }); +}); diff --git a/src/formula/functions/countif.ts b/src/formula/functions/countif.ts index 75957362..08130c41 100644 --- a/src/formula/functions/countif.ts +++ b/src/formula/functions/countif.ts @@ -2,7 +2,7 @@ import { FormulaError } from '../evaluator'; import { solveTable } from '../solver'; import { Table } from '../../lib/table'; import { BaseFunction } from './__base'; -import { check } from './__utils'; +import { check, ensureString } from './__utils'; export class CountifFunction extends BaseFunction { example = 'COUNTIF(A1:A10,">20")'; @@ -19,6 +19,7 @@ export class CountifFunction extends BaseFunction { if (this.bareArgs.length !== 2) { throw new FormulaError('#N/A', 'Number of arguments for COUNTIF is incorrect.'); } + this.bareArgs[1] = ensureString(this.bareArgs[1]); } protected main(table: Table, condition: string) { diff --git a/src/formula/functions/eq.spec.ts b/src/formula/functions/eq.spec.ts new file mode 100644 index 00000000..07ebd1de --- /dev/null +++ b/src/formula/functions/eq.spec.ts @@ -0,0 +1,91 @@ +import { EqFunction } from './eq'; +import { Table } from '../../lib/table'; +import { FormulaError, RefEntity, ValueEntity } from '../evaluator'; + +describe('eq', () => { + const table = new Table({}); + table.initialize({ + A1: { value: 101 }, + A2: { value: 101 }, + A3: { value: 103 }, + B1: { value: 'abc' }, + B2: { value: 'abcd' }, + C1: { value: new Date('2022-05-23T12:34:56+09:00') }, + C2: { value: new Date('2022-05-23T12:34:56.999+09:00') }, + C3: { value: new Date('2022-05-23T12:34:56Z') }, + D4: { value: '=1/0' }, + E5: { value: null }, + }); + + describe('normal', () => { + it('A1=101 is true', () => { + const f = new EqFunction({ + table, + args: [new RefEntity('A1'), new ValueEntity(101)], + }); + expect(f.call()).toBe(true); + }); + it('A1=A2 is true', () => { + const f = new EqFunction({ + table, + args: [new RefEntity('A1'), new RefEntity('A2')], + }); + expect(f.call()).toBe(true); + }); + it('A1=A3 is false', () => { + const f = new EqFunction({ + table, + args: [new RefEntity('A1'), new RefEntity('A3')], + }); + expect(f.call()).toBe(false); + }); + it('B1=abc is true', () => { + const f = new EqFunction({ + table, + args: [new RefEntity('B1'), new ValueEntity('abc')], + }); + expect(f.call()).toBe(true); + }); + it('B1=B2 is false', () => { + const f = new EqFunction({ + table, + args: [new RefEntity('B1'), new RefEntity('B2')], + }); + expect(f.call()).toBe(false); + }); + it('C1=raw date is true', () => { + const f = new EqFunction({ + table, + args: [new RefEntity('C1'), new ValueEntity(new Date('2022-05-23T12:34:56+09:00'))], + }); + expect(f.call()).toBe(true); + }); + it('C1=C2 is false', () => { + const f = new EqFunction({ + table, + args: [new RefEntity('C1'), new RefEntity('C2')], + }); + expect(f.call()).toBe(false); + }); + it('C1=C3 is false', () => { + const f = new EqFunction({ + table, + args: [new RefEntity('C1'), new RefEntity('C3')], + }); + expect(f.call()).toBe(false); + }); + it('null is blank', () => { + const f = new EqFunction({ + table, + args: [new ValueEntity(null), new ValueEntity('')], + }); + expect(f.call()).toBe(true); + }); + }); + describe('validation error', () => { + it('missing argument', () => { + const f = new EqFunction({ table, args: [] }); + expect(f.call.bind(f)).toThrow(FormulaError); + }); + }); +}); diff --git a/src/formula/functions/eq.ts b/src/formula/functions/eq.ts index 01351308..5e320663 100644 --- a/src/formula/functions/eq.ts +++ b/src/formula/functions/eq.ts @@ -1,5 +1,6 @@ import { FormulaError } from '../evaluator'; import { BaseFunction } from './__base'; +import { eq } from './__utils'; export class EqFunction extends BaseFunction { example = 'EQ(6, 7)'; @@ -18,7 +19,7 @@ export class EqFunction extends BaseFunction { } } - protected main(v1: number, v2: number) { - return v1 === v2; + protected main(v1: any, v2: any) { + return eq(v1, v2); } } diff --git a/src/formula/functions/gt.ts b/src/formula/functions/gt.ts index 23916763..0e063fe2 100644 --- a/src/formula/functions/gt.ts +++ b/src/formula/functions/gt.ts @@ -1,6 +1,6 @@ import { FormulaError } from '../evaluator'; import { BaseFunction } from './__base'; -import { ensureNumber } from './__utils'; +import { gt } from './__utils'; export class GtFunction extends BaseFunction { example = 'GT(5, 3)'; @@ -17,10 +17,9 @@ export class GtFunction extends BaseFunction { if (this.bareArgs.length !== 2) { throw new FormulaError('#N/A', 'Number of arguments for GT is incorrect.'); } - this.bareArgs = this.bareArgs.map((arg) => ensureNumber(arg)); } protected main(v1: number, v2: number) { - return v1 > v2; + return gt(v1, v2); } } diff --git a/src/formula/functions/gte.ts b/src/formula/functions/gte.ts index 768ad4c8..5cb7e1aa 100644 --- a/src/formula/functions/gte.ts +++ b/src/formula/functions/gte.ts @@ -1,6 +1,6 @@ import { FormulaError } from '../evaluator'; import { BaseFunction } from './__base'; -import { ensureNumber } from './__utils'; +import { gte } from './__utils'; export class GteFunction extends BaseFunction { example = 'GTE(5, 3)'; @@ -17,10 +17,9 @@ export class GteFunction extends BaseFunction { if (this.bareArgs.length !== 2) { throw new FormulaError('#N/A', 'Number of arguments for GTE is incorrect.'); } - this.bareArgs = this.bareArgs.map((arg) => ensureNumber(arg)); } protected main(v1: number, v2: number) { - return v1 >= v2; + return gte(v1, v2); } } diff --git a/src/formula/functions/lt.ts b/src/formula/functions/lt.ts index 33922b9b..a90dd468 100644 --- a/src/formula/functions/lt.ts +++ b/src/formula/functions/lt.ts @@ -1,6 +1,6 @@ import { FormulaError } from '../evaluator'; import { BaseFunction } from './__base'; -import { ensureNumber } from './__utils'; +import { lt } from './__utils'; export class LtFunction extends BaseFunction { example = 'LT(3, 6)'; @@ -17,10 +17,9 @@ export class LtFunction extends BaseFunction { if (this.bareArgs.length !== 2) { throw new FormulaError('#N/A', 'Number of arguments for LT is incorrect.'); } - this.bareArgs = this.bareArgs.map((arg) => ensureNumber(arg)); } protected main(v1: number, v2: number) { - return v1 < v2; + return lt(v1, v2); } } diff --git a/src/formula/functions/lte.ts b/src/formula/functions/lte.ts index 0165ff65..7caa38d2 100644 --- a/src/formula/functions/lte.ts +++ b/src/formula/functions/lte.ts @@ -1,6 +1,6 @@ import { FormulaError } from '../evaluator'; import { BaseFunction } from './__base'; -import { ensureNumber } from './__utils'; +import { lte } from './__utils'; export class LteFunction extends BaseFunction { example = 'LTE(3, 6)'; @@ -17,10 +17,9 @@ export class LteFunction extends BaseFunction { if (this.bareArgs.length !== 2) { throw new FormulaError('#N/A', 'Number of arguments for LTE is incorrect.'); } - this.bareArgs = this.bareArgs.map((arg) => ensureNumber(arg)); } protected main(v1: number, v2: number) { - return v1 <= v2; + return lte(v1, v2); } } diff --git a/src/formula/functions/minus.ts b/src/formula/functions/minus.ts index b1f9da2e..9dc8dd88 100644 --- a/src/formula/functions/minus.ts +++ b/src/formula/functions/minus.ts @@ -1,10 +1,11 @@ -import { subSeconds } from 'date-fns'; +import dayjs from 'dayjs'; import { FormulaError } from '../evaluator'; import { TimeDelta } from '../../lib/time'; import { BaseFunction } from './__base'; import { ensureNumber, stripTable } from './__utils'; import { Table } from '../../lib/table'; +import { SECONDS_IN_DAY } from '../../constants'; export class MinusFunction extends BaseFunction { example = 'MINUS(8, 3)'; @@ -40,7 +41,9 @@ export class MinusFunction extends BaseFunction { return v1.sub(v2); } if (v1 instanceof Date && typeof v2 === 'number') { - return subSeconds(v1, v2); + return dayjs(v1) + .subtract(v2 * SECONDS_IN_DAY, 'second') + .toDate(); } if (!v1) { return -v2; diff --git a/src/formula/functions/ne.ts b/src/formula/functions/ne.ts index b811ef4d..4e87b715 100644 --- a/src/formula/functions/ne.ts +++ b/src/formula/functions/ne.ts @@ -1,5 +1,6 @@ import { FormulaError } from '../evaluator'; import { BaseFunction } from './__base'; +import { ne } from './__utils'; export class NeFunction extends BaseFunction { example = 'NE(6, 7)'; @@ -19,6 +20,6 @@ export class NeFunction extends BaseFunction { } protected main(v1: number, v2: number) { - return v1 !== v2; + return ne(v1, v2); } } diff --git a/src/formula/functions/sumif.ts b/src/formula/functions/sumif.ts index 613ec81c..67557701 100644 --- a/src/formula/functions/sumif.ts +++ b/src/formula/functions/sumif.ts @@ -2,7 +2,7 @@ import { FormulaError } from '../evaluator'; import { solveTable } from '../solver'; import { Table } from '../../lib/table'; import { BaseFunction } from './__base'; -import { check } from './__utils'; +import { check, ensureString } from './__utils'; import { AreaType } from '../../types'; export class SumifFunction extends BaseFunction { @@ -28,6 +28,7 @@ export class SumifFunction extends BaseFunction { if (this.bareArgs[2] != undefined && this.bareArgs[2] instanceof Table) { throw new FormulaError('#N/A', '3rd argument must be range.'); } + this.bareArgs[1] = ensureString(this.bareArgs[1]); } protected main(range: Table, condition: string, sumRange: Table) { diff --git a/src/lib/autofill.ts b/src/lib/autofill.ts index 36209e37..c6aacabe 100644 --- a/src/lib/autofill.ts +++ b/src/lib/autofill.ts @@ -1,5 +1,5 @@ import React from 'react'; -import { isEqual } from 'date-fns'; +import dayjs from 'dayjs'; import type { AreaType, CellsByAddressType, CellType, PointType, StoreType } from '../types'; import { Table } from '../lib/table'; import { areaShape, areaToZone, complementSelectingArea, concatAreas, zoneToArea } from './structs'; @@ -379,7 +379,7 @@ class TypedGroup { switch (this.kind) { case 'date': { // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - const eq = this.nexts.every((v, i) => i === 0 || isEqual(v, this.timeDelta.add(this.nexts[i - 1]))); + const eq = this.nexts.every((v, i) => i === 0 || dayjs(v).isSame(this.timeDelta.add(this.nexts[i - 1]))); this.equidistant = eq; return []; } diff --git a/src/lib/time.ts b/src/lib/time.ts index 17791997..da817247 100644 --- a/src/lib/time.ts +++ b/src/lib/time.ts @@ -1,24 +1,14 @@ -import { - addYears, - addMonths, - addDays, - addHours, - addMinutes, - addSeconds, - addMilliseconds, - subYears, - subMonths, - subDays, - subHours, - subMinutes, - subSeconds, - subMilliseconds, -} from 'date-fns'; +import dayjs from 'dayjs'; export const BASE_DATE = new Date('2345-01-02T03:04:05Z'); type DiffFunction = (date: Date | number, amount: number) => Date; -const ADD_FNS = [addYears, addMonths, addDays, addHours, addMinutes, addSeconds, addMilliseconds] as DiffFunction[]; -const SUB_FNS = [subYears, subMonths, subDays, subHours, subMinutes, subSeconds, subMilliseconds] as DiffFunction[]; +const UNITS = ['year', 'month', 'day', 'hour', 'minute', 'second', 'millisecond'] as const; +const ADD_FNS = UNITS.map( + (unit) => (date: Date, amount: number) => dayjs(date).add(amount, unit).toDate(), +) as DiffFunction[]; +const SUB_FNS = UNITS.map( + (unit) => (date: Date, amount: number) => dayjs(date).subtract(amount, unit).toDate(), +) as DiffFunction[]; type Diff = [number, number, number, number, number, number, number]; @@ -40,7 +30,7 @@ export class TimeDelta { ]; this.date1 = date1; this.date2 = date2; - this.format = 'HH:mm'; + this.format = 'HH:mm:ss'; } public add(date: Date) { this.diff.forEach((n, i) => { diff --git a/src/parsers/core.ts b/src/parsers/core.ts index f857e481..78f85c91 100644 --- a/src/parsers/core.ts +++ b/src/parsers/core.ts @@ -1,6 +1,10 @@ -import { parseFromTimeZone } from 'date-fns-timezone'; import { CellType } from '../types'; import { TimeDelta } from '../lib/time'; +import dayjs from 'dayjs'; +import utc from 'dayjs/plugin/utc'; +import timezone from 'dayjs/plugin/timezone'; +dayjs.extend(timezone); +dayjs.extend(utc); type Condition = (value: string) => boolean; type Stringify = (value: string) => any; @@ -12,6 +16,10 @@ type Props = { }; const BOOLS = { true: true, false: false } as { [s: string]: boolean }; +const NUMS = new Set(['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']); +const NUMS_Z = new Set([...NUMS, 'Z', 'z']); +const JFMASOND = new Set(['J', 'F', 'M', 'A', 'S', 'O', 'N', 'D', ...NUMS]); +const NBRYNLGPTVC = new Set(['N', 'B', 'R', 'Y', 'N', 'L', 'G', 'P', 'T', 'V', 'C', ...NUMS_Z]); export interface ParserMixinType { functions?: ((value: string, cell?: CellType) => any)[]; @@ -138,10 +146,10 @@ export class Parser implements ParserMixinType { // eslint-disable-next-line @typescript-eslint/no-unused-vars date(value: string, cell?: CellType): Date | undefined { const first = value[0]; - if (first == null || first.match(/[JFMASOND0-9]/) == null) { + if (first == null || !JFMASOND.has(first.toUpperCase())) { return; } - if (value[value.length - 1].match(/[0-9Z]/) == null) { + if (!NBRYNLGPTVC.has(value[value.length - 1].toUpperCase())) { return; } if (value.match(/[=*&#@!?[\]{}"'()|%\\<>~+\r\n]/)) { @@ -153,11 +161,12 @@ export class Parser implements ParserMixinType { } catch (e) { // eslint-disable-next-line no-empty } - const d = parseFromTimeZone(value, { timeZone }); - if (d.toString() === 'Invalid Date') { - return; + try { + const day = dayjs.tz(value, timeZone); + return day.toDate(); + } catch (e) { + // eslint-disable-next-line no-empty } - return d; } } diff --git a/src/renderers/core.ts b/src/renderers/core.ts index 68a731f4..4e39d87a 100644 --- a/src/renderers/core.ts +++ b/src/renderers/core.ts @@ -1,10 +1,11 @@ +import dayjs from 'dayjs'; + import { CellType, PointType, WriterType } from '../types'; import { Table, UserTable } from '../lib/table'; import { solveFormula } from '../formula/solver'; import { FormulaError } from '../formula/evaluator'; import { p2a } from '../lib/converters'; import { TimeDelta } from '../lib/time'; -import { format as formatDate } from 'date-fns'; // eslint-disable-next-line @typescript-eslint/no-explicit-any type Condition = (value: any) => boolean; @@ -32,9 +33,9 @@ export interface RendererMixinType { } export class Renderer implements RendererMixinType { - public datetimeFormat: string = 'yyyy-MM-dd HH:mm:ss'; - public dateFormat: string = 'yyyy-MM-dd'; - public timeDeltaFormat: string = 'HH:mm'; + public datetimeFormat: string = 'YYYY-MM-DD HH:mm:ss'; + public dateFormat: string = 'YYYY-MM-DD'; + public timeDeltaFormat: string = 'HH:mm:ss'; private condition?: Condition; private complement?: Stringify; @@ -162,9 +163,9 @@ export class Renderer implements RendererMixinType { // eslint-disable-next-line @typescript-eslint/no-unused-vars date(value: Date, writer?: WriterType): any { if (value.getHours() + value.getMinutes() + value.getSeconds() === 0) { - return formatDate(value, this.dateFormat); + return dayjs(value).format(this.dateFormat); } - return formatDate(value, this.datetimeFormat); + return dayjs(value).format(this.datetimeFormat); } // eslint-disable-next-line @typescript-eslint/no-unused-vars diff --git a/yarn.lock b/yarn.lock index 39bd703a..3f30e2bd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5622,11 +5622,6 @@ comma-separated-tokens@^1.0.0: resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz#632b80b6117867a158f1080ad498b2fbe7e3f5ea" integrity sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw== -commander@2.19.0: - version "2.19.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.19.0.tgz#f6198aa84e5b83c46054b94ddedbfed5ee9ff12a" - integrity sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg== - commander@2.8.x: version "2.8.1" resolved "https://registry.yarnpkg.com/commander/-/commander-2.8.1.tgz#06be367febfda0c330aa1e2a072d3dc9762425d4" @@ -6038,28 +6033,15 @@ data-urls@^3.0.2: whatwg-mimetype "^3.0.0" whatwg-url "^11.0.0" -date-fns-timezone@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/date-fns-timezone/-/date-fns-timezone-0.1.4.tgz#bc9fac78aae9a7bdb847f3ab4ce6d14f9fb9a55f" - integrity sha512-npnZn1eIeHV8A3Hqw86mn6CjH+qMe0TSCs4anpD4Rouf+mE9eIJuaHviIpNmGL9GiDmcUoiaKdkX5ihf+yZQZA== - dependencies: - date-fns "^1.29.0" - timezone-support "^1.5.5" - date-fns@*: version "3.3.1" resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-3.3.1.tgz#7581daca0892d139736697717a168afbb908cfed" integrity sha512-y8e109LYGgoQDveiEBD3DYXKba1jWf5BA8YU1FL5Tvm0BTdEfy54WLCwnuYWZNnzzvALy/QQ4Hov+Q9RVRv+Zw== -date-fns@^1.29.0: - version "1.30.1" - resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-1.30.1.tgz#2e71bf0b119153dbb4cc4e88d9ea5acfb50dc05c" - integrity sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw== - -date-fns@^2.28.0: - version "2.29.3" - resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.29.3.tgz#27402d2fc67eb442b511b70bbdf98e6411cd68a8" - integrity sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA== +dayjs@^1.11.13: + version "1.11.13" + resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.13.tgz#92430b0139055c3ebb60150aa13e860a4b5a366c" + integrity sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg== debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.9: version "2.6.9" @@ -12431,13 +12413,6 @@ timers-browserify@^2.0.4: dependencies: setimmediate "^1.0.4" -timezone-support@^1.5.5: - version "1.8.1" - resolved "https://registry.yarnpkg.com/timezone-support/-/timezone-support-1.8.1.tgz#0a8c04d0614be6fccdd2280aaad5072fa5d31e5f" - integrity sha512-+pKzxoUe4PZXaQcswceJlA+69oRyyu1uivnYKdpsC7eGzZiuvTLbU4WYPqTKslEsoSvjN8k/u/6qNfGikBB/wA== - dependencies: - commander "2.19.0" - tmpl@1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.5.tgz#8683e0b902bb9c20c4f726e3c0b69f36518c07cc"