mirror of
https://github.com/mayswind/ezbookkeeping.git
synced 2026-05-13 22:47:33 +08:00
replace Jest with Vitest
This commit is contained in:
@@ -1,21 +0,0 @@
|
|||||||
import { type JestConfigWithTsJest, createDefaultEsmPreset } from 'ts-jest';
|
|
||||||
|
|
||||||
const presetConfig = createDefaultEsmPreset({
|
|
||||||
tsconfig: '<rootDir>/tsconfig.jest.json'
|
|
||||||
});
|
|
||||||
|
|
||||||
const config: JestConfigWithTsJest = {
|
|
||||||
...presetConfig,
|
|
||||||
clearMocks: true,
|
|
||||||
collectCoverage: false,
|
|
||||||
moduleNameMapper: {
|
|
||||||
"^@/(.*)$": "<rootDir>/src/$1"
|
|
||||||
},
|
|
||||||
testEnvironment: "node",
|
|
||||||
testMatch: [
|
|
||||||
"**/__tests__/**/*.[jt]s?(x)",
|
|
||||||
"!**/__tests__/*_gen.[jt]s?(x)"
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
export default config;
|
|
||||||
Generated
+497
-2548
File diff suppressed because it is too large
Load Diff
+2
-4
@@ -16,7 +16,7 @@
|
|||||||
"build": "cross-env NODE_ENV=production vite build",
|
"build": "cross-env NODE_ENV=production vite build",
|
||||||
"serve:dist": "vite preview",
|
"serve:dist": "vite preview",
|
||||||
"lint": "vue-tsc --noEmit && eslint . --fix",
|
"lint": "vue-tsc --noEmit && eslint . --fix",
|
||||||
"test": "cross-env TS_NODE_PROJECT=\"./tsconfig.jest.json\" jest"
|
"test": "vitest run"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mdi/js": "7.4.47",
|
"@mdi/js": "7.4.47",
|
||||||
@@ -56,7 +56,6 @@
|
|||||||
"@types/crypto-js": "4.2.2",
|
"@types/crypto-js": "4.2.2",
|
||||||
"@types/git-rev-sync": "2.0.2",
|
"@types/git-rev-sync": "2.0.2",
|
||||||
"@types/jalaali-js": "1.2.0",
|
"@types/jalaali-js": "1.2.0",
|
||||||
"@types/jest": "30.0.0",
|
|
||||||
"@types/node": "25.6.0",
|
"@types/node": "25.6.0",
|
||||||
"@types/ua-parser-js": "0.7.39",
|
"@types/ua-parser-js": "0.7.39",
|
||||||
"@vitejs/plugin-vue": "6.0.6",
|
"@vitejs/plugin-vue": "6.0.6",
|
||||||
@@ -66,16 +65,15 @@
|
|||||||
"eslint": "10.2.1",
|
"eslint": "10.2.1",
|
||||||
"eslint-plugin-vue": "10.9.0",
|
"eslint-plugin-vue": "10.9.0",
|
||||||
"git-rev-sync": "3.0.2",
|
"git-rev-sync": "3.0.2",
|
||||||
"jest": "30.3.0",
|
|
||||||
"postcss-preset-env": "11.2.1",
|
"postcss-preset-env": "11.2.1",
|
||||||
"sass": "1.99.0",
|
"sass": "1.99.0",
|
||||||
"ts-jest": "29.4.9",
|
|
||||||
"ts-node": "10.9.2",
|
"ts-node": "10.9.2",
|
||||||
"typescript": "6.0.3",
|
"typescript": "6.0.3",
|
||||||
"vite": "7.3.2",
|
"vite": "7.3.2",
|
||||||
"vite-plugin-checker": "0.13.0",
|
"vite-plugin-checker": "0.13.0",
|
||||||
"vite-plugin-pwa": "1.2.0",
|
"vite-plugin-pwa": "1.2.0",
|
||||||
"vite-plugin-vuetify": "2.1.3",
|
"vite-plugin-vuetify": "2.1.3",
|
||||||
|
"vitest": "4.1.5",
|
||||||
"vue-tsc": "3.2.7"
|
"vue-tsc": "3.2.7"
|
||||||
},
|
},
|
||||||
"browserslist": [
|
"browserslist": [
|
||||||
|
|||||||
@@ -0,0 +1,225 @@
|
|||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import { describe, expect, it, beforeAll } from 'vitest';
|
||||||
|
import moment from 'moment-timezone';
|
||||||
|
|
||||||
|
import type { TextualYearMonth } from '@/core/datetime.ts';
|
||||||
|
import { FiscalYearStart, FiscalYearUnixTime } from '@/core/fiscalyear.ts';
|
||||||
|
|
||||||
|
import {
|
||||||
|
getFiscalYearFromUnixTime,
|
||||||
|
getFiscalYearStartUnixTime,
|
||||||
|
getFiscalYearEndUnixTime,
|
||||||
|
getFiscalYearTimeRangeFromUnixTime,
|
||||||
|
getAllFiscalYearsStartAndEndUnixTimes,
|
||||||
|
getFiscalYearTimeRangeFromYear
|
||||||
|
} from '@/lib/datetime.ts';
|
||||||
|
|
||||||
|
// Set test environment timezone to UTC, since the test data constants are in UTC
|
||||||
|
beforeAll(() => {
|
||||||
|
moment.tz.setDefault('UTC');
|
||||||
|
});
|
||||||
|
|
||||||
|
function importTestData(datasetName: string): unknown[] {
|
||||||
|
const data = JSON.parse(
|
||||||
|
fs.readFileSync(path.join(__dirname, 'fiscal_year.data.json'), 'utf8')
|
||||||
|
);
|
||||||
|
if (!data || typeof data[datasetName] === 'undefined') {
|
||||||
|
throw new Error(`${datasetName} is undefined or missing in the data object.`);
|
||||||
|
}
|
||||||
|
return data[datasetName];
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatUnixTimeISO(unixTime: number): string {
|
||||||
|
return moment.unix(unixTime).format('YYYY-MM-DDTHH:mm:ssZ');
|
||||||
|
}
|
||||||
|
|
||||||
|
function withISO(data: FiscalYearUnixTime) {
|
||||||
|
return {
|
||||||
|
...data,
|
||||||
|
minUnixTimeISO: formatUnixTimeISO(data.minUnixTime),
|
||||||
|
maxUnixTimeISO: formatUnixTimeISO(data.maxUnixTime),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
type FiscalYearStartConfig = {
|
||||||
|
id: string;
|
||||||
|
monthDateString: string;
|
||||||
|
value: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const FISCAL_YEAR_START_PRESETS: Record<string, FiscalYearStartConfig> = {
|
||||||
|
'January 1': { id: 'January 1', monthDateString: '01-01', value: 0x0101 },
|
||||||
|
'April 1': { id: 'April 1', monthDateString: '04-01', value: 0x0401 },
|
||||||
|
'October 1': { id: 'October 1', monthDateString: '10-01', value: 0x0A01 },
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('validateFiscalYearStart', () => {
|
||||||
|
Object.values(FISCAL_YEAR_START_PRESETS).forEach(({ id, value, monthDateString }) => {
|
||||||
|
it(`should return a fiscal year start object for valid value 0x${value.toString(16)} (${id})`, () => {
|
||||||
|
expect(FiscalYearStart.valueOf(value)).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`should return the correct month-date string for valid value 0x${value.toString(16)} (${id})`, () => {
|
||||||
|
expect(FiscalYearStart.valueOf(value)?.toMonthDashDayString()).toStrictEqual(monthDateString);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const INVALID_FISCAL_YEAR_VALUES = [
|
||||||
|
0x0000, // Invalid: L0/0
|
||||||
|
0x0D01, // Invalid: Month 13
|
||||||
|
0x0100, // Invalid: Day 0
|
||||||
|
0x0120, // Invalid: January 32
|
||||||
|
0x021D, // Invalid: February 29 (not permitted)
|
||||||
|
0x021E, // Invalid: February 30
|
||||||
|
0x041F, // Invalid: April 31
|
||||||
|
0x061F, // Invalid: June 31
|
||||||
|
0x091F, // Invalid: September 31
|
||||||
|
0x0B20, // Invalid: November 32
|
||||||
|
0xFFFF, // Invalid: Largest uint16
|
||||||
|
];
|
||||||
|
|
||||||
|
describe('validateFiscalYearStartInvalidValues', () => {
|
||||||
|
INVALID_FISCAL_YEAR_VALUES.forEach((value) => {
|
||||||
|
it(`should return undefined for invalid fiscal year start value 0x${value.toString(16)}`, () => {
|
||||||
|
expect(FiscalYearStart.valueOf(value)).not.toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('validateFiscalYearStartLeapDay', () => {
|
||||||
|
it('should return undefined for February 29 value (0x021D)', () => {
|
||||||
|
expect(FiscalYearStart.valueOf(0x021D)).not.toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return undefined when parsing month-day string "02-29"', () => {
|
||||||
|
expect(FiscalYearStart.parse('02-29')).not.toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
type FiscalYearFromUnixTimeCase = {
|
||||||
|
date: string;
|
||||||
|
unixTime: number;
|
||||||
|
expected: { [fiscalYearStartId: string]: number };
|
||||||
|
};
|
||||||
|
|
||||||
|
const TEST_CASES_GET_FISCAL_YEAR_FROM_UNIX_TIME =
|
||||||
|
importTestData('test_cases_getFiscalYearFromUnixTime') as FiscalYearFromUnixTimeCase[];
|
||||||
|
|
||||||
|
describe('getFiscalYearFromUnixTime', () => {
|
||||||
|
Object.values(FISCAL_YEAR_START_PRESETS).forEach(({ id, value }) => {
|
||||||
|
TEST_CASES_GET_FISCAL_YEAR_FROM_UNIX_TIME.forEach((testCase) => {
|
||||||
|
it(`should return correct fiscal year for FY_START ${id}, date ${moment(testCase.date).format('MMMM D, YYYY')}`, () => {
|
||||||
|
expect(getFiscalYearFromUnixTime(moment(testCase.date).unix(), value)).toBe(testCase.expected[id]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
type FiscalYearStartUnixTimeCase = {
|
||||||
|
date: string;
|
||||||
|
expected: {
|
||||||
|
[fiscalYearStart: string]: { unixTime: number; unixTimeISO: string };
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const TEST_CASES_GET_FISCAL_YEAR_START_UNIX_TIME =
|
||||||
|
importTestData('test_cases_getFiscalYearStartUnixTime') as FiscalYearStartUnixTimeCase[];
|
||||||
|
|
||||||
|
describe('getFiscalYearStartUnixTime', () => {
|
||||||
|
Object.values(FISCAL_YEAR_START_PRESETS).forEach(({ id, value }) => {
|
||||||
|
TEST_CASES_GET_FISCAL_YEAR_START_UNIX_TIME.forEach((testCase) => {
|
||||||
|
it(`should return correct start unix time for FY_START ${id}, date ${moment(testCase.date).format('MMMM D, YYYY')}`, () => {
|
||||||
|
const startUnixTime = getFiscalYearStartUnixTime(moment(testCase.date).unix(), value);
|
||||||
|
const expected = testCase.expected[id];
|
||||||
|
expect({ unixTime: startUnixTime, ISO: formatUnixTimeISO(startUnixTime) })
|
||||||
|
.toStrictEqual({ unixTime: expected!.unixTime, ISO: expected!.unixTimeISO });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
type FiscalYearEndUnixTimeCase = {
|
||||||
|
date: string;
|
||||||
|
expected: {
|
||||||
|
[fiscalYearStart: string]: { unixTime: number; unixTimeISO: string };
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const TEST_CASES_GET_FISCAL_YEAR_END_UNIX_TIME =
|
||||||
|
importTestData('test_cases_getFiscalYearEndUnixTime') as FiscalYearEndUnixTimeCase[];
|
||||||
|
|
||||||
|
describe('getFiscalYearEndUnixTime', () => {
|
||||||
|
Object.values(FISCAL_YEAR_START_PRESETS).forEach(({ id, value }) => {
|
||||||
|
TEST_CASES_GET_FISCAL_YEAR_END_UNIX_TIME.forEach((testCase) => {
|
||||||
|
it(`should return correct end unix time for FY_START ${id}, date ${moment(testCase.date).format('MMMM D, YYYY')}`, () => {
|
||||||
|
const endUnixTime = getFiscalYearEndUnixTime(moment(testCase.date).unix(), value);
|
||||||
|
const expected = testCase.expected[id];
|
||||||
|
expect({ unixTime: endUnixTime, ISO: formatUnixTimeISO(endUnixTime) })
|
||||||
|
.toStrictEqual({ unixTime: expected!.unixTime, ISO: expected!.unixTimeISO });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
type FiscalYearTimeRangeFromUnixTimeCase = {
|
||||||
|
date: string;
|
||||||
|
expected: { [fiscalYearStart: string]: FiscalYearUnixTime[] };
|
||||||
|
};
|
||||||
|
|
||||||
|
const TEST_CASES_GET_FISCAL_YEAR_UNIX_TIME_RANGE =
|
||||||
|
importTestData('test_cases_getFiscalYearTimeRangeFromUnixTime') as FiscalYearTimeRangeFromUnixTimeCase[];
|
||||||
|
|
||||||
|
describe('getFiscalYearTimeRangeFromUnixTime', () => {
|
||||||
|
Object.values(FISCAL_YEAR_START_PRESETS).forEach(({ id, value }) => {
|
||||||
|
TEST_CASES_GET_FISCAL_YEAR_UNIX_TIME_RANGE.forEach((testCase) => {
|
||||||
|
it(`should return correct fiscal year unix time range for FY_START ${id}, date ${moment(testCase.date).format('MMMM D, YYYY')}`, () => {
|
||||||
|
expect(getFiscalYearTimeRangeFromUnixTime(moment(testCase.date).unix(), value))
|
||||||
|
.toStrictEqual(testCase.expected[id]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
type AllFiscalYearsStartAndEndUnixTimesCase = {
|
||||||
|
startYearMonth: TextualYearMonth;
|
||||||
|
endYearMonth: TextualYearMonth;
|
||||||
|
fiscalYearStart: string;
|
||||||
|
fiscalYearStartId: string;
|
||||||
|
expected: FiscalYearUnixTime[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const TEST_CASES_GET_ALL_FISCAL_YEARS_START_AND_END_UNIX_TIMES =
|
||||||
|
importTestData('test_cases_getAllFiscalYearsStartAndEndUnixTimes') as AllFiscalYearsStartAndEndUnixTimesCase[];
|
||||||
|
|
||||||
|
describe('getAllFiscalYearsStartAndEndUnixTimes', () => {
|
||||||
|
TEST_CASES_GET_ALL_FISCAL_YEARS_START_AND_END_UNIX_TIMES.forEach((testCase) => {
|
||||||
|
it(`should return correct fiscal year start and end unix times for FY_START ${testCase.fiscalYearStartId}, range ${testCase.startYearMonth} to ${testCase.endYearMonth}`, () => {
|
||||||
|
const fiscalYearStart = FiscalYearStart.parse(testCase.fiscalYearStart);
|
||||||
|
expect(fiscalYearStart).toBeDefined();
|
||||||
|
expect(getAllFiscalYearsStartAndEndUnixTimes(testCase.startYearMonth, testCase.endYearMonth, fiscalYearStart?.value || 0).map(withISO))
|
||||||
|
.toStrictEqual(testCase.expected.map(withISO));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
type FiscalYearTimeRangeFromYearCase = {
|
||||||
|
year: number;
|
||||||
|
fiscalYearStart: string;
|
||||||
|
expected: FiscalYearUnixTime;
|
||||||
|
};
|
||||||
|
|
||||||
|
const TEST_CASES_GET_FISCAL_YEAR_RANGE_FROM_YEAR =
|
||||||
|
importTestData('test_cases_getFiscalYearTimeRangeFromYear') as FiscalYearTimeRangeFromYearCase[];
|
||||||
|
|
||||||
|
describe('getFiscalYearTimeRangeFromYear', () => {
|
||||||
|
TEST_CASES_GET_FISCAL_YEAR_RANGE_FROM_YEAR.forEach((testCase) => {
|
||||||
|
it(`should return correct fiscal year unix time range for year ${testCase.year} and FY_START ${testCase.fiscalYearStart}`, () => {
|
||||||
|
const fiscalYearStart = FiscalYearStart.parse(testCase.fiscalYearStart);
|
||||||
|
expect(fiscalYearStart).toBeDefined();
|
||||||
|
expect(getFiscalYearTimeRangeFromYear(testCase.year, fiscalYearStart?.value || 0))
|
||||||
|
.toStrictEqual(testCase.expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,289 +0,0 @@
|
|||||||
// Unit tests for fiscal year functions
|
|
||||||
import fs from 'fs';
|
|
||||||
import path from 'path';
|
|
||||||
import { describe, expect, test, beforeAll } from '@jest/globals';
|
|
||||||
import moment from 'moment-timezone';
|
|
||||||
|
|
||||||
// Import all the fiscal year functions from the lib
|
|
||||||
import type { TextualYearMonth } from '@/core/datetime.ts';
|
|
||||||
import { FiscalYearStart, FiscalYearUnixTime } from '@/core/fiscalyear.ts';
|
|
||||||
|
|
||||||
import {
|
|
||||||
getFiscalYearFromUnixTime,
|
|
||||||
getFiscalYearStartUnixTime,
|
|
||||||
getFiscalYearEndUnixTime,
|
|
||||||
getFiscalYearTimeRangeFromUnixTime,
|
|
||||||
getAllFiscalYearsStartAndEndUnixTimes,
|
|
||||||
getFiscalYearTimeRangeFromYear
|
|
||||||
} from '@/lib/datetime.ts';
|
|
||||||
|
|
||||||
// Set test environment timezone to UTC, since the test data constants are in UTC
|
|
||||||
beforeAll(() => {
|
|
||||||
moment.tz.setDefault('UTC');
|
|
||||||
});
|
|
||||||
|
|
||||||
// UTILITIES
|
|
||||||
function importTestData(datasetName: string): unknown[] {
|
|
||||||
const data = JSON.parse(
|
|
||||||
fs.readFileSync(path.join(__dirname, 'fiscal_year.data.json'), 'utf8')
|
|
||||||
);
|
|
||||||
if (!data || typeof data[datasetName] === 'undefined') {
|
|
||||||
throw new Error(`${datasetName} is undefined or missing in the data object.`);
|
|
||||||
}
|
|
||||||
return data[datasetName];
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatUnixTimeISO(unixTime: number): string {
|
|
||||||
return moment.unix(unixTime).format('YYYY-MM-DDTHH:mm:ssZ');
|
|
||||||
}
|
|
||||||
|
|
||||||
function getTestTitleFormatDate(testFiscalYearStartId: string, testCaseDateString: string): string {
|
|
||||||
return `FY_START: ${testFiscalYearStartId.padStart(10, ' ')}; DATE: ${moment(testCaseDateString).format('MMMM D, YYYY')}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getTestTitleFormatString(testFiscalYearStartId: string, testCaseString: string): string {
|
|
||||||
return `FY_START: ${testFiscalYearStartId.padStart(10, ' ')}; ${testCaseString}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// FISCAL YEAR START CONFIGURATION
|
|
||||||
type FiscalYearStartConfig = {
|
|
||||||
id: string;
|
|
||||||
monthDateString: string;
|
|
||||||
value: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
const TEST_FISCAL_YEAR_START_PRESETS: Record<string, FiscalYearStartConfig> = {
|
|
||||||
'January 1': {
|
|
||||||
id: 'January 1',
|
|
||||||
monthDateString: '01-01',
|
|
||||||
value: 0x0101,
|
|
||||||
},
|
|
||||||
'April 1': {
|
|
||||||
id: 'April 1',
|
|
||||||
monthDateString: '04-01',
|
|
||||||
value: 0x0401,
|
|
||||||
},
|
|
||||||
'October 1': {
|
|
||||||
id: 'October 1',
|
|
||||||
monthDateString: '10-01',
|
|
||||||
value: 0x0A01,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// VALIDATE FISCAL YEAR START PRESETS
|
|
||||||
describe('validateFiscalYearStart', () => {
|
|
||||||
Object.values(TEST_FISCAL_YEAR_START_PRESETS).forEach((testFiscalYearStart) => {
|
|
||||||
test(`should return fiscal year start object if fiscal year start value (uint16) is valid: id: ${testFiscalYearStart.id}; value: 0x${testFiscalYearStart.value.toString(16)}`, () => {
|
|
||||||
expect(FiscalYearStart.valueOf(testFiscalYearStart.value)).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
test(`returns same month-date string for valid fiscal year start value: id: ${testFiscalYearStart.id}; value: 0x${testFiscalYearStart.value.toString(16)}`, () => {
|
|
||||||
const fiscalYearStart = FiscalYearStart.valueOf(testFiscalYearStart.value);
|
|
||||||
expect(fiscalYearStart?.toMonthDashDayString()).toStrictEqual(testFiscalYearStart.monthDateString);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
// VALIDATE INVALID FISCAL YEAR START VALUES
|
|
||||||
const TestCase_invalidFiscalYearValues = [
|
|
||||||
0x0000, // Invalid: L0/0
|
|
||||||
0x0D01, // Invalid: Month 13
|
|
||||||
0x0100, // Invalid: Day 0
|
|
||||||
0x0120, // Invalid: January 32
|
|
||||||
0x021D, // Invalid: February 29 (not permitted)
|
|
||||||
0x021E, // Invalid: February 30
|
|
||||||
0x041F, // Invalid: April 31
|
|
||||||
0x061F, // Invalid: June 31
|
|
||||||
0x091F, // Invalid: September 31
|
|
||||||
0x0B20, // Invalid: November 32
|
|
||||||
0xFFFF, // Invalid: Largest uint16
|
|
||||||
]
|
|
||||||
|
|
||||||
describe('validateFiscalYearStartInvalidValues', () => {
|
|
||||||
TestCase_invalidFiscalYearValues.forEach((testCase) => {
|
|
||||||
test(`should return undefined if fiscal year start value (uint16) is invalid: value: 0x${testCase.toString(16)}`, () => {
|
|
||||||
expect(FiscalYearStart.valueOf(testCase)).not.toBeDefined();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// VALIDATE LEAP DAY FEBRUARY 29 IS NOT VALID
|
|
||||||
describe('validateFiscalYearStartLeapDay', () => {
|
|
||||||
test(`should return undefined if fiscal year start value (uint16) for February 29 is invalid: value: 0x0229}`, () => {
|
|
||||||
expect(FiscalYearStart.valueOf(0x021D)).not.toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
test(`should return undefined if fiscal year month-day string "02-29" is used to create fiscal year start object`, () => {
|
|
||||||
expect(FiscalYearStart.parse('02-29')).not.toBeDefined();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// FISCAL YEAR FROM UNIX TIME
|
|
||||||
type TestCase_getFiscalYearFromUnixTime = {
|
|
||||||
date: string;
|
|
||||||
unixTime: number;
|
|
||||||
expected: {
|
|
||||||
[fiscalYearStartId: string]: number;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const TEST_CASES_GET_FISCAL_YEAR_FROM_UNIX_TIME: TestCase_getFiscalYearFromUnixTime[] =
|
|
||||||
importTestData('test_cases_getFiscalYearFromUnixTime') as TestCase_getFiscalYearFromUnixTime[];
|
|
||||||
|
|
||||||
describe('getFiscalYearFromUnixTime', () => {
|
|
||||||
Object.values(TEST_FISCAL_YEAR_START_PRESETS).forEach((testFiscalYearStart) => {
|
|
||||||
TEST_CASES_GET_FISCAL_YEAR_FROM_UNIX_TIME.forEach((testCase) => {
|
|
||||||
test(`returns correct fiscal year for ${getTestTitleFormatDate(testFiscalYearStart.id, testCase.date)}`, () => {
|
|
||||||
const testCaseUnixTime = moment(testCase.date).unix();
|
|
||||||
const fiscalYear = getFiscalYearFromUnixTime(testCaseUnixTime, testFiscalYearStart.value);
|
|
||||||
const expected = testCase.expected[testFiscalYearStart.id];
|
|
||||||
expect(fiscalYear).toBe(expected);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
// FISCAL YEAR START UNIX TIME
|
|
||||||
type TestCase_getFiscalYearStartUnixTime = {
|
|
||||||
date: string;
|
|
||||||
expected: {
|
|
||||||
[fiscalYearStart: string]: {
|
|
||||||
unixTime: number;
|
|
||||||
unixTimeISO: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const TEST_CASES_GET_FISCAL_YEAR_START_UNIX_TIME: TestCase_getFiscalYearStartUnixTime[] =
|
|
||||||
importTestData('test_cases_getFiscalYearStartUnixTime') as TestCase_getFiscalYearStartUnixTime[];
|
|
||||||
|
|
||||||
describe('getFiscalYearStartUnixTime', () => {
|
|
||||||
Object.values(TEST_FISCAL_YEAR_START_PRESETS).forEach((testFiscalYearStart) => {
|
|
||||||
TEST_CASES_GET_FISCAL_YEAR_START_UNIX_TIME.forEach((testCase) => {
|
|
||||||
test(`returns correct start unix time for ${getTestTitleFormatDate(testFiscalYearStart.id, testCase.date)}`, () => {
|
|
||||||
const testCaseUnixTime = moment(testCase.date).unix();
|
|
||||||
const startUnixTime = getFiscalYearStartUnixTime(testCaseUnixTime, testFiscalYearStart.value);
|
|
||||||
const expected = testCase.expected[testFiscalYearStart.id];
|
|
||||||
const unixTimeISO = formatUnixTimeISO(startUnixTime);
|
|
||||||
|
|
||||||
expect({ unixTime: startUnixTime, ISO: unixTimeISO }).toStrictEqual({ unixTime: expected!.unixTime, ISO: expected!.unixTimeISO });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
// FISCAL YEAR END UNIX TIME
|
|
||||||
type TestCase_getFiscalYearEndUnixTime = {
|
|
||||||
date: string;
|
|
||||||
expected: {
|
|
||||||
[fiscalYearStart: string]: {
|
|
||||||
unixTime: number;
|
|
||||||
unixTimeISO: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const TEST_CASES_GET_FISCAL_YEAR_END_UNIX_TIME: TestCase_getFiscalYearEndUnixTime[] =
|
|
||||||
importTestData('test_cases_getFiscalYearEndUnixTime') as TestCase_getFiscalYearEndUnixTime[];
|
|
||||||
|
|
||||||
describe('getFiscalYearEndUnixTime', () => {
|
|
||||||
Object.values(TEST_FISCAL_YEAR_START_PRESETS).forEach((testFiscalYearStart) => {
|
|
||||||
TEST_CASES_GET_FISCAL_YEAR_END_UNIX_TIME.forEach((testCase) => {
|
|
||||||
test(`returns correct end unix time for ${getTestTitleFormatDate(testFiscalYearStart.id, testCase.date)}`, () => {
|
|
||||||
const testCaseUnixTime = moment(testCase.date).unix();
|
|
||||||
const endUnixTime = getFiscalYearEndUnixTime(testCaseUnixTime, testFiscalYearStart.value);
|
|
||||||
const expected = testCase.expected[testFiscalYearStart.id];
|
|
||||||
const unixTimeISO = formatUnixTimeISO(endUnixTime);
|
|
||||||
|
|
||||||
expect({ unixTime: endUnixTime, ISO: unixTimeISO }).toStrictEqual({ unixTime: expected!.unixTime, ISO: expected!.unixTimeISO });
|
|
||||||
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// GET FISCAL YEAR UNIX TIME RANGE
|
|
||||||
type TestCase_getFiscalYearTimeRangeFromUnixTime = {
|
|
||||||
date: string;
|
|
||||||
expected: {
|
|
||||||
[fiscalYearStart: string]: FiscalYearUnixTime[]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const TEST_CASES_GET_FISCAL_YEAR_UNIX_TIME_RANGE: TestCase_getFiscalYearTimeRangeFromUnixTime[] =
|
|
||||||
importTestData('test_cases_getFiscalYearTimeRangeFromUnixTime') as TestCase_getFiscalYearTimeRangeFromUnixTime[];
|
|
||||||
|
|
||||||
describe('getFiscalYearTimeRangeFromUnixTime', () => {
|
|
||||||
Object.values(TEST_FISCAL_YEAR_START_PRESETS).forEach((testFiscalYearStart) => {
|
|
||||||
TEST_CASES_GET_FISCAL_YEAR_UNIX_TIME_RANGE.forEach((testCase) => {
|
|
||||||
test(`returns correct fiscal year unix time range for ${getTestTitleFormatDate(testFiscalYearStart.id, testCase.date)}`, () => {
|
|
||||||
const testCaseUnixTime = moment(testCase.date).unix();
|
|
||||||
const fiscalYearUnixTimeRange = getFiscalYearTimeRangeFromUnixTime(testCaseUnixTime, testFiscalYearStart.value);
|
|
||||||
expect(fiscalYearUnixTimeRange).toStrictEqual(testCase.expected[testFiscalYearStart.id]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// GET ALL FISCAL YEAR START AND END UNIX TIMES
|
|
||||||
type TestCase_getAllFiscalYearsStartAndEndUnixTimes = {
|
|
||||||
startYearMonth: TextualYearMonth;
|
|
||||||
endYearMonth: TextualYearMonth;
|
|
||||||
fiscalYearStart: string;
|
|
||||||
fiscalYearStartId: string;
|
|
||||||
expected: FiscalYearUnixTime[]
|
|
||||||
}
|
|
||||||
|
|
||||||
const TEST_CASES_GET_ALL_FISCAL_YEARS_START_AND_END_UNIX_TIMES: TestCase_getAllFiscalYearsStartAndEndUnixTimes[] =
|
|
||||||
importTestData('test_cases_getAllFiscalYearsStartAndEndUnixTimes') as TestCase_getAllFiscalYearsStartAndEndUnixTimes[];
|
|
||||||
|
|
||||||
describe('getAllFiscalYearsStartAndEndUnixTimes', () => {
|
|
||||||
TEST_CASES_GET_ALL_FISCAL_YEARS_START_AND_END_UNIX_TIMES.forEach((testCase) => {
|
|
||||||
const fiscalYearStart = FiscalYearStart.parse(testCase.fiscalYearStart);
|
|
||||||
test(`returns correct fiscal year start and end unix times for ${getTestTitleFormatString(testCase.fiscalYearStartId, `${testCase.startYearMonth} to ${testCase.endYearMonth}`)}`, () => {
|
|
||||||
expect(fiscalYearStart).toBeDefined();
|
|
||||||
|
|
||||||
const fiscalYearStartAndEndUnixTimes = getAllFiscalYearsStartAndEndUnixTimes(testCase.startYearMonth, testCase.endYearMonth, fiscalYearStart?.value || 0);
|
|
||||||
|
|
||||||
// Convert results to include ISO strings for better test output
|
|
||||||
const resultWithISO = fiscalYearStartAndEndUnixTimes.map(data => ({
|
|
||||||
...data,
|
|
||||||
minUnixTimeISO: formatUnixTimeISO(data.minUnixTime),
|
|
||||||
maxUnixTimeISO: formatUnixTimeISO(data.maxUnixTime)
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Convert expected to include ISO strings
|
|
||||||
const expectedWithISO = testCase.expected.map(data => ({
|
|
||||||
...data,
|
|
||||||
minUnixTimeISO: formatUnixTimeISO(data.minUnixTime),
|
|
||||||
maxUnixTimeISO: formatUnixTimeISO(data.maxUnixTime)
|
|
||||||
}));
|
|
||||||
|
|
||||||
expect(resultWithISO).toStrictEqual(expectedWithISO);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// GET FISCAL YEAR RANGE FROM YEAR
|
|
||||||
type TestCase_getFiscalYearTimeRangeFromYear = {
|
|
||||||
year: number;
|
|
||||||
fiscalYearStart: string;
|
|
||||||
expected: FiscalYearUnixTime;
|
|
||||||
}
|
|
||||||
|
|
||||||
const TEST_CASES_GET_FISCAL_YEAR_RANGE_FROM_YEAR: TestCase_getFiscalYearTimeRangeFromYear[] =
|
|
||||||
importTestData('test_cases_getFiscalYearTimeRangeFromYear') as TestCase_getFiscalYearTimeRangeFromYear[];
|
|
||||||
|
|
||||||
describe('getFiscalYearTimeRangeFromYear', () => {
|
|
||||||
TEST_CASES_GET_FISCAL_YEAR_RANGE_FROM_YEAR.forEach((testCase) => {
|
|
||||||
const fiscalYearStart = FiscalYearStart.parse(testCase.fiscalYearStart);
|
|
||||||
test(`returns correct fiscal year unix time range for input year integer ${testCase.year} and FY_START: ${testCase.fiscalYearStart}`, () => {
|
|
||||||
expect(fiscalYearStart).toBeDefined();
|
|
||||||
const fiscalYearRange = getFiscalYearTimeRangeFromYear(testCase.year, fiscalYearStart?.value || 0);
|
|
||||||
expect(fiscalYearRange).toStrictEqual(testCase.expected);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
import { mean, median, percentile, sumMaxN } from '@/lib/math.ts';
|
||||||
|
|
||||||
|
describe('mean', () => {
|
||||||
|
it('should return zero for empty array', () => {
|
||||||
|
expect(mean([], item => item)).toBeCloseTo(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the average for positive values', () => {
|
||||||
|
expect(mean([1, 2, 3, 4], item => item)).toBeCloseTo(2.5);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the average for negative and positive values', () => {
|
||||||
|
expect(mean([-10, 0, 20], item => item)).toBeCloseTo(10 / 3);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('median', () => {
|
||||||
|
it('should return zero for empty sorted array', () => {
|
||||||
|
expect(median([], item => item)).toBeCloseTo(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the middle value for odd-length sorted array', () => {
|
||||||
|
expect(median([1, 3, 5], item => item)).toBeCloseTo(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the average of the two middle values for even-length sorted array', () => {
|
||||||
|
expect(median([1, 3, 5, 7], item => item)).toBeCloseTo(4);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('percentile', () => {
|
||||||
|
it('should return zero for empty sorted array', () => {
|
||||||
|
expect(percentile([], 0.5, item => item)).toBeCloseTo(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return zero when percentile is smaller than zero', () => {
|
||||||
|
expect(percentile([1, 2, 3], -0.1, item => item)).toBeCloseTo(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return zero when percentile is larger than one', () => {
|
||||||
|
expect(percentile([1, 2, 3], 1.1, item => item)).toBeCloseTo(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the minimum value for zero percentile', () => {
|
||||||
|
expect(percentile([5, 10, 15, 20], 0, item => item)).toBeCloseTo(5);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the maximum value for one percentile', () => {
|
||||||
|
expect(percentile([5, 10, 15, 20], 1, item => item)).toBeCloseTo(20);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the exact indexed value when percentile maps to an integer index', () => {
|
||||||
|
expect(percentile([10, 20, 30, 40, 50], 0.25, item => item)).toBeCloseTo(20);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should interpolate between neighboring values when percentile maps to a fractional index', () => {
|
||||||
|
expect(percentile([10, 20, 30, 40, 50, 60, 70, 80], 0.25, item => item)).toBeCloseTo(27.5);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('sumMaxN', () => {
|
||||||
|
it('should return zero for empty sorted array', () => {
|
||||||
|
expect(sumMaxN([], 3, item => item)).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return zero when n is zero', () => {
|
||||||
|
expect(sumMaxN([1, 2, 3], 0, item => item)).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the sum of the largest n values', () => {
|
||||||
|
expect(sumMaxN([1, 2, 3, 4, 5], 2, item => item)).toBe(9);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the sum of all values when n is larger than array length', () => {
|
||||||
|
expect(sumMaxN([1, 2, 3, 4], 10, item => item)).toBe(10);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,190 +0,0 @@
|
|||||||
import { describe, expect, test } from '@jest/globals';
|
|
||||||
|
|
||||||
import { mean, median, percentile, sumMaxN } from '@/lib/math.ts';
|
|
||||||
|
|
||||||
type TestNumberItem = {
|
|
||||||
value: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
function createNumberItems(values: number[]): TestNumberItem[] {
|
|
||||||
return values.map(value => ({ value }));
|
|
||||||
}
|
|
||||||
|
|
||||||
function getTestTitle(functionName: string, title: string): string {
|
|
||||||
return `${functionName}: ${title}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
type MeanTestCase = {
|
|
||||||
title: string;
|
|
||||||
values: number[];
|
|
||||||
expected: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
const TEST_CASES_MEAN: MeanTestCase[] = [
|
|
||||||
{
|
|
||||||
title: 'returns zero for empty array',
|
|
||||||
values: [],
|
|
||||||
expected: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'returns the average for positive values',
|
|
||||||
values: [1, 2, 3, 4],
|
|
||||||
expected: 2.5,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'returns the average for negative and positive values',
|
|
||||||
values: [-10, 0, 20],
|
|
||||||
expected: 10 / 3,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
describe('mean', () => {
|
|
||||||
TEST_CASES_MEAN.forEach((testCase) => {
|
|
||||||
test(getTestTitle('mean', testCase.title), () => {
|
|
||||||
const result = mean(createNumberItems(testCase.values), item => item.value);
|
|
||||||
expect(result).toBeCloseTo(testCase.expected);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
type MedianTestCase = {
|
|
||||||
title: string;
|
|
||||||
values: number[];
|
|
||||||
expected: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
const TEST_CASES_MEDIAN: MedianTestCase[] = [
|
|
||||||
{
|
|
||||||
title: 'returns zero for empty sorted array',
|
|
||||||
values: [],
|
|
||||||
expected: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'returns the middle value for odd-length sorted array',
|
|
||||||
values: [1, 3, 5],
|
|
||||||
expected: 3,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'returns the average of the two middle values for even-length sorted array',
|
|
||||||
values: [1, 3, 5, 7],
|
|
||||||
expected: 4,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
describe('median', () => {
|
|
||||||
TEST_CASES_MEDIAN.forEach((testCase) => {
|
|
||||||
test(getTestTitle('median', testCase.title), () => {
|
|
||||||
const result = median(createNumberItems(testCase.values), item => item.value);
|
|
||||||
expect(result).toBeCloseTo(testCase.expected);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
type PercentileTestCase = {
|
|
||||||
title: string;
|
|
||||||
values: number[];
|
|
||||||
percentileValue: number;
|
|
||||||
expected: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
const TEST_CASES_PERCENTILE: PercentileTestCase[] = [
|
|
||||||
{
|
|
||||||
title: 'returns zero for empty sorted array',
|
|
||||||
values: [],
|
|
||||||
percentileValue: 0.5,
|
|
||||||
expected: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'returns zero when percentile is smaller than zero',
|
|
||||||
values: [1, 2, 3],
|
|
||||||
percentileValue: -0.1,
|
|
||||||
expected: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'returns zero when percentile is larger than one',
|
|
||||||
values: [1, 2, 3],
|
|
||||||
percentileValue: 1.1,
|
|
||||||
expected: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'returns the minimum value for zero percentile',
|
|
||||||
values: [5, 10, 15, 20],
|
|
||||||
percentileValue: 0,
|
|
||||||
expected: 5,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'returns the maximum value for one percentile',
|
|
||||||
values: [5, 10, 15, 20],
|
|
||||||
percentileValue: 1,
|
|
||||||
expected: 20,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'returns the exact indexed value when percentile maps to an integer index',
|
|
||||||
values: [10, 20, 30, 40, 50],
|
|
||||||
percentileValue: 0.25,
|
|
||||||
expected: 20,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'interpolates between neighboring values when percentile maps to a fractional index',
|
|
||||||
values: [10, 20, 30, 40, 50, 60, 70, 80],
|
|
||||||
percentileValue: 0.25,
|
|
||||||
expected: 27.5,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
describe('percentile', () => {
|
|
||||||
TEST_CASES_PERCENTILE.forEach((testCase) => {
|
|
||||||
test(getTestTitle('percentile', testCase.title), () => {
|
|
||||||
const result = percentile(
|
|
||||||
createNumberItems(testCase.values),
|
|
||||||
testCase.percentileValue,
|
|
||||||
item => item.value
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(result).toBeCloseTo(testCase.expected);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
type SumMaxNTestCase = {
|
|
||||||
title: string;
|
|
||||||
values: number[];
|
|
||||||
n: number;
|
|
||||||
expected: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
const TEST_CASES_SUM_MAX_N: SumMaxNTestCase[] = [
|
|
||||||
{
|
|
||||||
title: 'returns zero for empty sorted array',
|
|
||||||
values: [],
|
|
||||||
n: 3,
|
|
||||||
expected: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'returns zero when n is zero',
|
|
||||||
values: [1, 2, 3],
|
|
||||||
n: 0,
|
|
||||||
expected: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'returns the sum of the largest n values',
|
|
||||||
values: [1, 2, 3, 4, 5],
|
|
||||||
n: 2,
|
|
||||||
expected: 9,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'returns the sum of all values when n is larger than array length',
|
|
||||||
values: [1, 2, 3, 4],
|
|
||||||
n: 10,
|
|
||||||
expected: 10,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
describe('sumMaxN', () => {
|
|
||||||
TEST_CASES_SUM_MAX_N.forEach((testCase) => {
|
|
||||||
test(getTestTitle('sumMaxN', testCase.title), () => {
|
|
||||||
const result = sumMaxN(createNumberItems(testCase.values), testCase.n, item => item.value);
|
|
||||||
expect(result).toBe(testCase.expected);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
+40
-39
@@ -1,13 +1,12 @@
|
|||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { describe, expect, test } from '@jest/globals';
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
import { DEFAULT_CONTENT } from '@/locales/calendar/chinese/index.ts';
|
import { DEFAULT_CONTENT } from '@/locales/calendar/chinese/index.ts';
|
||||||
|
|
||||||
import { itemAndIndex, entries } from '@/core/base.ts';
|
import { itemAndIndex, entries } from '@/core/base.ts';
|
||||||
import type { ChineseCalendarLocaleData } from '@/core/calendar.ts';
|
import type { ChineseCalendarLocaleData } from '@/core/calendar.ts';
|
||||||
import {
|
import {
|
||||||
type ChineseYearMonthDayInfo,
|
|
||||||
getChineseYearMonthAllDayInfos,
|
getChineseYearMonthAllDayInfos,
|
||||||
getChineseYearMonthDayInfo
|
getChineseYearMonthDayInfo
|
||||||
} from '@/lib/calendar/chinese_calendar.ts';
|
} from '@/lib/calendar/chinese_calendar.ts';
|
||||||
@@ -45,12 +44,33 @@ const localeData: ChineseCalendarLocaleData = {
|
|||||||
'Winter Solstice'
|
'Winter Solstice'
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
const ordinalSuffix = ['st', 'nd', 'rd'];
|
const ordinalSuffix = ['st', 'nd', 'rd'];
|
||||||
|
|
||||||
describe('getChineseYearMonthAllDayInfos', () => {
|
function lunarMonthOrDayLabel(month: number, day: number): string {
|
||||||
const lines: string[] = fs.readFileSync(path.join(__dirname, 'chinese_calendar_all_data.txt'), 'utf8').replace(/\r/g, '').split('\n');
|
return day === 1
|
||||||
|
? `${month}${ordinalSuffix[month - 1] ?? 'th'} Lunar Month`.toLowerCase()
|
||||||
|
: day.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
type PerDayEntry = {
|
||||||
|
gregorianDate: string;
|
||||||
|
gregorianYear: number;
|
||||||
|
gregorianMonth: number;
|
||||||
|
gregorianDay: number;
|
||||||
|
expectedChineseMonthOrDay: string;
|
||||||
|
expectedSolarTermName: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function parseCalendarDataFile(): {
|
||||||
|
allMonthChineseDays: Record<string, string[]>;
|
||||||
|
allMonthSolarTermNames: Record<string, string[]>;
|
||||||
|
perDayEntries: PerDayEntry[];
|
||||||
|
} {
|
||||||
|
const lines = fs.readFileSync(path.join(__dirname, 'chinese_calendar_all_data.txt'), 'utf8').replace(/\r/g, '').split('\n');
|
||||||
const allMonthChineseDays: Record<string, string[]> = {};
|
const allMonthChineseDays: Record<string, string[]> = {};
|
||||||
const allMonthSolarTermNames: Record<string, string[]> = {};
|
const allMonthSolarTermNames: Record<string, string[]> = {};
|
||||||
|
const perDayEntries: PerDayEntry[] = [];
|
||||||
let currentMonthChineseDays: string[] = [];
|
let currentMonthChineseDays: string[] = [];
|
||||||
let currentMonthSolarTermNames: string[] = [];
|
let currentMonthSolarTermNames: string[] = [];
|
||||||
let currentYear: number = 0;
|
let currentYear: number = 0;
|
||||||
@@ -66,16 +86,17 @@ describe('getChineseYearMonthAllDayInfos', () => {
|
|||||||
const gregorianDateItems = gregorianDate.split('/');
|
const gregorianDateItems = gregorianDate.split('/');
|
||||||
const gregorianYear = parseInt(gregorianDateItems[0] as string, 10);
|
const gregorianYear = parseInt(gregorianDateItems[0] as string, 10);
|
||||||
const gregorianMonth = parseInt(gregorianDateItems[1] as string, 10);
|
const gregorianMonth = parseInt(gregorianDateItems[1] as string, 10);
|
||||||
|
const gregorianDay = parseInt(gregorianDateItems[2] as string, 10);
|
||||||
const chineseDay = items[1] as string;
|
const chineseDay = items[1] as string;
|
||||||
const solarTermName = items.length > 3 ? items[3] as string : '';
|
const solarTermName = items.length > 3 ? items[3] as string : '';
|
||||||
|
|
||||||
|
perDayEntries.push({ gregorianDate, gregorianYear, gregorianMonth, gregorianDay, expectedChineseMonthOrDay: chineseDay, expectedSolarTermName: solarTermName });
|
||||||
|
|
||||||
if (currentYear > 0 && currentMonth > 0 && (gregorianYear !== currentYear || gregorianMonth !== currentMonth)) {
|
if (currentYear > 0 && currentMonth > 0 && (gregorianYear !== currentYear || gregorianMonth !== currentMonth)) {
|
||||||
allMonthChineseDays[`${currentYear}-${currentMonth}`] = currentMonthChineseDays;
|
allMonthChineseDays[`${currentYear}-${currentMonth}`] = currentMonthChineseDays;
|
||||||
allMonthSolarTermNames[`${currentYear}-${currentMonth}`] = currentMonthSolarTermNames;
|
allMonthSolarTermNames[`${currentYear}-${currentMonth}`] = currentMonthSolarTermNames;
|
||||||
|
|
||||||
currentMonthChineseDays = [];
|
currentMonthChineseDays = [];
|
||||||
currentMonthSolarTermNames = [];
|
currentMonthSolarTermNames = [];
|
||||||
|
|
||||||
currentYear = gregorianYear;
|
currentYear = gregorianYear;
|
||||||
currentMonth = gregorianMonth;
|
currentMonth = gregorianMonth;
|
||||||
} else if (currentYear === 0 && currentMonth === 0) {
|
} else if (currentYear === 0 && currentMonth === 0) {
|
||||||
@@ -92,27 +113,27 @@ describe('getChineseYearMonthAllDayInfos', () => {
|
|||||||
allMonthChineseDays[`${currentYear}-${currentMonth}`] = currentMonthChineseDays;
|
allMonthChineseDays[`${currentYear}-${currentMonth}`] = currentMonthChineseDays;
|
||||||
allMonthSolarTermNames[`${currentYear}-${currentMonth}`] = currentMonthSolarTermNames;
|
allMonthSolarTermNames[`${currentYear}-${currentMonth}`] = currentMonthSolarTermNames;
|
||||||
|
|
||||||
|
return { allMonthChineseDays, allMonthSolarTermNames, perDayEntries };
|
||||||
|
}
|
||||||
|
|
||||||
|
const { allMonthChineseDays, allMonthSolarTermNames, perDayEntries } = parseCalendarDataFile();
|
||||||
|
|
||||||
|
describe('getChineseYearMonthAllDayInfos', () => {
|
||||||
for (const [yearMonth, monthChineseDays] of entries(allMonthChineseDays)) {
|
for (const [yearMonth, monthChineseDays] of entries(allMonthChineseDays)) {
|
||||||
test(`returns correct chinese all dates in month for ${yearMonth}`, () => {
|
it(`should return correct chinese dates for all days in ${yearMonth}`, () => {
|
||||||
const [yearStr, monthStr] = yearMonth.split('-');
|
const [yearStr, monthStr] = yearMonth.split('-');
|
||||||
const year = parseInt(yearStr as string);
|
const year = parseInt(yearStr as string);
|
||||||
const month = parseInt(monthStr as string);
|
const month = parseInt(monthStr as string);
|
||||||
const expectedChineseMonthOrDays = monthChineseDays;
|
|
||||||
const expectedSolarTermNames = allMonthSolarTermNames[yearMonth] as string[];
|
const expectedSolarTermNames = allMonthSolarTermNames[yearMonth] as string[];
|
||||||
|
|
||||||
const actualChineseDates: ChineseYearMonthDayInfo[] | undefined = getChineseYearMonthAllDayInfos({
|
const actualChineseDates = getChineseYearMonthAllDayInfos({ year, month1base: month }, localeData);
|
||||||
year: year,
|
|
||||||
month1base: month
|
|
||||||
}, localeData);
|
|
||||||
|
|
||||||
expect(actualChineseDates).toBeDefined();
|
expect(actualChineseDates).toBeDefined();
|
||||||
|
|
||||||
if (actualChineseDates) {
|
if (actualChineseDates) {
|
||||||
for (const [actualChineseDate, index] of itemAndIndex(actualChineseDates)) {
|
for (const [actualChineseDate, index] of itemAndIndex(actualChineseDates)) {
|
||||||
const chineseMonthOrDay: string | undefined = actualChineseDate?.day === 1 ? `${actualChineseDate?.month}${ordinalSuffix[actualChineseDate?.month - 1] ?? 'th'} Lunar Month`.toLowerCase() : actualChineseDate?.day.toString();
|
|
||||||
|
|
||||||
expect(actualChineseDate).toBeDefined();
|
expect(actualChineseDate).toBeDefined();
|
||||||
expect(chineseMonthOrDay).toBe(expectedChineseMonthOrDays[index]);
|
expect(lunarMonthOrDayLabel(actualChineseDate!.month, actualChineseDate!.day)).toBe(monthChineseDays[index]);
|
||||||
expect(actualChineseDate?.solarTermName).toBe(expectedSolarTermNames[index]);
|
expect(actualChineseDate?.solarTermName).toBe(expectedSolarTermNames[index]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -121,32 +142,12 @@ describe('getChineseYearMonthAllDayInfos', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('getChineseYearMonthDayInfo', () => {
|
describe('getChineseYearMonthDayInfo', () => {
|
||||||
const lines: string[] = fs.readFileSync(path.join(__dirname, 'chinese_calendar_all_data.txt'), 'utf8').replace(/\r/g, '').split('\n');
|
for (const { gregorianDate, gregorianYear, gregorianMonth, gregorianDay, expectedChineseMonthOrDay, expectedSolarTermName } of perDayEntries) {
|
||||||
|
it(`should return correct chinese date for ${gregorianDate}`, () => {
|
||||||
for (const line of lines) {
|
const actualChineseDate = getChineseYearMonthDayInfo({ year: gregorianYear, month: gregorianMonth, day: gregorianDay }, localeData);
|
||||||
if (!line.trim() || line.startsWith('#')) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const items = line.split('\t');
|
|
||||||
const gregorianDate = items[0] as string;
|
|
||||||
const gregorianDateItems = gregorianDate.split('/');
|
|
||||||
const gregorianYear = parseInt(gregorianDateItems[0] as string);
|
|
||||||
const gregorianMonth = parseInt(gregorianDateItems[1] as string);
|
|
||||||
const gregorianDay = parseInt(gregorianDateItems[2] as string);
|
|
||||||
const expectedChineseMonthOrDay = items[1] as string;
|
|
||||||
const expectedSolarTermName = items.length > 3 ? items[3] as string : '';
|
|
||||||
|
|
||||||
test(`returns correct chinese date for ${gregorianDate}`, () => {
|
|
||||||
const actualChineseDate: ChineseYearMonthDayInfo | undefined = getChineseYearMonthDayInfo({
|
|
||||||
year: gregorianYear,
|
|
||||||
month: gregorianMonth,
|
|
||||||
day: gregorianDay
|
|
||||||
}, localeData);
|
|
||||||
const actualChineseMonthOrDay: string | undefined = actualChineseDate?.day === 1 ? `${actualChineseDate?.month}${ordinalSuffix[actualChineseDate?.month - 1] ?? 'th'} Lunar Month`.toLowerCase() : actualChineseDate?.day.toString();
|
|
||||||
|
|
||||||
expect(actualChineseDate).toBeDefined();
|
expect(actualChineseDate).toBeDefined();
|
||||||
expect(actualChineseMonthOrDay).toBe(expectedChineseMonthOrDay.toLowerCase());
|
expect(lunarMonthOrDayLabel(actualChineseDate!.month, actualChineseDate!.day)).toBe(expectedChineseMonthOrDay.toLowerCase());
|
||||||
expect(actualChineseDate?.solarTermName).toBe(expectedSolarTermName);
|
expect(actualChineseDate?.solarTermName).toBe(expectedSolarTermName);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "./tsconfig.json",
|
|
||||||
"compilerOptions": {
|
|
||||||
"verbatimModuleSyntax": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+1
-1
@@ -18,6 +18,6 @@
|
|||||||
"include": [
|
"include": [
|
||||||
"src/**/*",
|
"src/**/*",
|
||||||
"vite.config.ts",
|
"vite.config.ts",
|
||||||
"jest.config.ts"
|
"vitest.config.ts"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { defineConfig } from 'vitest/config';
|
||||||
|
import vue from '@vitejs/plugin-vue';
|
||||||
|
import path from 'node:path';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [vue()],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': path.resolve(__dirname, './src')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
test: {
|
||||||
|
environment: 'node',
|
||||||
|
clearMocks: true,
|
||||||
|
restoreMocks: true,
|
||||||
|
mockReset: true,
|
||||||
|
isolate: true,
|
||||||
|
coverage: {
|
||||||
|
provider: 'v8',
|
||||||
|
reporter: ['text', 'html']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user