mirror of
https://github.com/mayswind/ezbookkeeping.git
synced 2026-05-17 00:12:11 +08:00
Feature - Add support for a fiscal year period defined in user settings.
* Add "This fiscal year", "Last fiscal year" as date range options in Transaction Details to filter transactions to those periods * Add fiscal year ranges to Statistics & Trend Analysis * Add "fiscal year start date" to user profile settings, allowing the user to select any date of the calendar year as the start of the fiscal year * Add "fiscal year format" to user profile settings, allowing the user to specify how financial year date labels should appear Implementation notes: * The default fiscal year start is January 1 and the default fiscal year display format is "FY 2025" * Fiscal year start date (month number & day number) are stored together in db as a uint16, high byte & low byte respectively * February 29 is disallowed as a fiscal year start date, since it is never used as a convention in any country * Jest is added to the project as a dev dependency, for unit tests in frontend Signed-off-by: Sebastian Reategui <seb.reategui@gmail.com>
This commit is contained in:
committed by
mayswind
parent
70eea8ff33
commit
b94dc8eb83
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,291 @@
|
||||
// Unit tests for fiscal year functions
|
||||
import moment from 'moment-timezone';
|
||||
import { describe, expect, test, beforeAll } from '@jest/globals';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
// Import all the fiscal year functions from the lib
|
||||
import {
|
||||
getFiscalYearFromUnixTime,
|
||||
getFiscalYearStartUnixTime,
|
||||
getFiscalYearEndUnixTime,
|
||||
getFiscalYearTimeRangeFromUnixTime,
|
||||
getAllFiscalYearsStartAndEndUnixTimes,
|
||||
getFiscalYearTimeRangeFromYear
|
||||
} from '@/lib/datetime.ts';
|
||||
|
||||
import { formatUnixTime } from '@/lib/datetime.ts';
|
||||
import { FiscalYearStart, FiscalYearUnixTime } from '@/core/fiscalyear.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): any[] {
|
||||
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 formatUnixTime(unixTime, 'YYYY-MM-DD[T]HH:mm:ss[Z]');
|
||||
}
|
||||
|
||||
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 true if fiscal year start value (uint16) is valid: id: ${testFiscalYearStart.id}; value: 0x${testFiscalYearStart.value.toString(16)}`, () => {
|
||||
expect(FiscalYearStart.isValidType(testFiscalYearStart.value)).toBe(true);
|
||||
});
|
||||
|
||||
test(`returns same month-date string for valid fiscal year start value: id: ${testFiscalYearStart.id}; value: 0x${testFiscalYearStart.value.toString(16)}`, () => {
|
||||
const fiscalYearStart = FiscalYearStart.strictFromNumber(testFiscalYearStart.value);
|
||||
expect(fiscalYearStart.toString()).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 false if fiscal year start value (uint16) is invalid: value: 0x${testCase.toString(16)}`, () => {
|
||||
expect(FiscalYearStart.isValidType(testCase)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// VALIDATE LEAP DAY FEBRUARY 29 IS NOT VALID
|
||||
describe('validateFiscalYearStartLeapDay', () => {
|
||||
test(`should return false if fiscal year start value (uint16) for February 29 is invalid: value: 0x0229}`, () => {
|
||||
expect(FiscalYearStart.isValidType(0x0229)).toBe(false);
|
||||
});
|
||||
|
||||
test(`should return error if fiscal year month-day string "02-29" is used to create fiscal year start object`, () => {
|
||||
expect(() => FiscalYearStart.strictFromMonthDashDayString('02-29')).toThrow();
|
||||
});
|
||||
|
||||
test(`should return error if integers "02" and "29" are used to create fiscal year start object`, () => {
|
||||
expect(() => FiscalYearStart.validateMonthDay(2, 29)).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
// FISCAL YEAR FROM UNIX TIME
|
||||
type TestCase_getFiscalYearFromUnixTime = {
|
||||
date: string;
|
||||
unixTime: number;
|
||||
expected: {
|
||||
[fiscalYearStartId: string]: number;
|
||||
};
|
||||
};
|
||||
|
||||
let TEST_CASES_GET_FISCAL_YEAR_FROM_UNIX_TIME: TestCase_getFiscalYearFromUnixTime[];
|
||||
TEST_CASES_GET_FISCAL_YEAR_FROM_UNIX_TIME = 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;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
let TEST_CASES_GET_FISCAL_YEAR_START_UNIX_TIME: TestCase_getFiscalYearStartUnixTime[];
|
||||
TEST_CASES_GET_FISCAL_YEAR_START_UNIX_TIME = 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;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
let TEST_CASES_GET_FISCAL_YEAR_END_UNIX_TIME: TestCase_getFiscalYearEndUnixTime[];
|
||||
TEST_CASES_GET_FISCAL_YEAR_END_UNIX_TIME = 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[]
|
||||
}
|
||||
}
|
||||
|
||||
let TEST_CASES_GET_FISCAL_YEAR_UNIX_TIME_RANGE: TestCase_getFiscalYearTimeRangeFromUnixTime[];
|
||||
TEST_CASES_GET_FISCAL_YEAR_UNIX_TIME_RANGE = 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: string;
|
||||
endYearMonth: string;
|
||||
fiscalYearStart: string;
|
||||
fiscalYearStartId: string;
|
||||
expected: FiscalYearUnixTime[]
|
||||
}
|
||||
|
||||
let TEST_CASES_GET_ALL_FISCAL_YEARS_START_AND_END_UNIX_TIMES: TestCase_getAllFiscalYearsStartAndEndUnixTimes[];
|
||||
TEST_CASES_GET_ALL_FISCAL_YEARS_START_AND_END_UNIX_TIMES = 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.strictFromMonthDashDayString(testCase.fiscalYearStart);
|
||||
test(`returns correct fiscal year start and end unix times for ${getTestTitleFormatString(testCase.fiscalYearStartId, `${testCase.startYearMonth} to ${testCase.endYearMonth}`)}`, () => {
|
||||
const fiscalYearStartAndEndUnixTimes = getAllFiscalYearsStartAndEndUnixTimes(testCase.startYearMonth, testCase.endYearMonth, fiscalYearStart.value);
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
let TEST_CASES_GET_FISCAL_YEAR_RANGE_FROM_YEAR: TestCase_getFiscalYearTimeRangeFromYear[];
|
||||
TEST_CASES_GET_FISCAL_YEAR_RANGE_FROM_YEAR = importTestData('test_cases_getFiscalYearTimeRangeFromYear') as TestCase_getFiscalYearTimeRangeFromYear[];
|
||||
|
||||
describe('getFiscalYearTimeRangeFromYear', () => {
|
||||
TEST_CASES_GET_FISCAL_YEAR_RANGE_FROM_YEAR.forEach((testCase) => {
|
||||
const fiscalYearStart = FiscalYearStart.strictFromMonthDashDayString(testCase.fiscalYearStart);
|
||||
test(`returns correct fiscal year unix time range for input year integer ${testCase.year} and FY_START: ${testCase.fiscalYearStart}`, () => {
|
||||
const fiscalYearRange = getFiscalYearTimeRangeFromYear(testCase.year, fiscalYearStart.value);
|
||||
expect(fiscalYearRange).toStrictEqual(testCase.expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
+203
-19
@@ -22,6 +22,10 @@ import {
|
||||
DateRange,
|
||||
LANGUAGE_DEFAULT_DATE_TIME_FORMAT_VALUE
|
||||
} from '@/core/datetime.ts';
|
||||
import {
|
||||
type FiscalYearUnixTime,
|
||||
FiscalYearStart
|
||||
} from '@/core/fiscalyear.ts';
|
||||
import {
|
||||
isObject,
|
||||
isString,
|
||||
@@ -207,6 +211,10 @@ export function formatDate(date: string, format: string): string {
|
||||
return moment(date, 'YYYY-MM-DD').format(format);
|
||||
}
|
||||
|
||||
export function formatMonthDay(monthDay: string, format: string): string {
|
||||
return moment(monthDay, 'MM-DD').format(format);
|
||||
}
|
||||
|
||||
export function getUnixTime(date: SupportedDate): number {
|
||||
return moment(date).unix();
|
||||
}
|
||||
@@ -434,6 +442,64 @@ export function getAllYearsStartAndEndUnixTimes(startYearMonth: YearMonth | stri
|
||||
return allYearTimes;
|
||||
}
|
||||
|
||||
export function getAllFiscalYearsStartAndEndUnixTimes(startYearMonth: YearMonth | string, endYearMonth: YearMonth | string, fiscalYearStart: number): FiscalYearUnixTime[] {
|
||||
// user selects date range: start=2024-01 and end=2026-12
|
||||
// result should be 4x FiscalYearUnixTime made up of:
|
||||
// - 2024-01->2024-06 (FY 24) - input start year-month->end of fiscal year in which the input start year-month falls
|
||||
// - 2024-07->2025-06 (FY 25) - complete fiscal year
|
||||
// - 2025-07->2026-06 (FY 26) - complete fiscal year
|
||||
// - 2026-07->2026-12 (FY 27) - start of fiscal year->end of fiscal year in which the input end year-month falls
|
||||
|
||||
const allFiscalYearTimes: FiscalYearUnixTime[] = [];
|
||||
const range = getStartEndYearMonthRange(startYearMonth, endYearMonth);
|
||||
|
||||
if (!range) {
|
||||
return allFiscalYearTimes;
|
||||
}
|
||||
|
||||
const inputStartUnixTime = getYearMonthFirstUnixTime(range.startYearMonth);
|
||||
const inputEndUnixTime = getYearMonthLastUnixTime(range.endYearMonth);
|
||||
const fiscalYearStartMonth = FiscalYearStart.strictFromNumber(fiscalYearStart).month;
|
||||
|
||||
// Loop over 1 year before and 1 year after the input date range
|
||||
// to include fiscal years that start in the previous calendar year.
|
||||
for (let year = range.startYearMonth.year - 1; year <= range.endYearMonth.year + 1; year++) {
|
||||
const thisYearMonthUnixTime = getYearMonthFirstUnixTime({ year: year, month: fiscalYearStartMonth });
|
||||
const fiscalStartTime = getFiscalYearStartUnixTime(thisYearMonthUnixTime, fiscalYearStart);
|
||||
const fiscalEndTime = getFiscalYearEndUnixTime(thisYearMonthUnixTime, fiscalYearStart);
|
||||
|
||||
const fiscalYear = getFiscalYearFromUnixTime(fiscalStartTime, fiscalYearStart);
|
||||
|
||||
if (fiscalStartTime <= inputEndUnixTime && fiscalEndTime >= inputStartUnixTime) {
|
||||
let minUnixTime = fiscalStartTime;
|
||||
let maxUnixTime = fiscalEndTime;
|
||||
|
||||
// Cap the min and max unix times to the input date range
|
||||
if (minUnixTime < inputStartUnixTime) {
|
||||
minUnixTime = inputStartUnixTime;
|
||||
}
|
||||
|
||||
if (maxUnixTime > inputEndUnixTime) {
|
||||
maxUnixTime = inputEndUnixTime;
|
||||
}
|
||||
|
||||
const fiscalYearTime: FiscalYearUnixTime = {
|
||||
year: fiscalYear,
|
||||
minUnixTime: minUnixTime,
|
||||
maxUnixTime: maxUnixTime,
|
||||
};
|
||||
|
||||
allFiscalYearTimes.push(fiscalYearTime);
|
||||
}
|
||||
|
||||
if (fiscalStartTime > inputEndUnixTime) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return allFiscalYearTimes;
|
||||
}
|
||||
|
||||
export function getAllQuartersStartAndEndUnixTimes(startYearMonth: YearMonth | string, endYearMonth: YearMonth | string): YearQuarterUnixTime[] {
|
||||
const allYearQuarterTimes: YearQuarterUnixTime[] = [];
|
||||
const range = getStartEndYearMonthRange(startYearMonth, endYearMonth);
|
||||
@@ -551,9 +617,9 @@ export function getShiftedDateRange(minTime: number, maxTime: number, scale: num
|
||||
};
|
||||
}
|
||||
|
||||
export function getShiftedDateRangeAndDateType(minTime: number, maxTime: number, scale: number, firstDayOfWeek: number, scene: DateRangeScene): TimeRangeAndDateType {
|
||||
export function getShiftedDateRangeAndDateType(minTime: number, maxTime: number, scale: number, firstDayOfWeek: number, fiscalYearStart: number, scene: DateRangeScene): TimeRangeAndDateType {
|
||||
const newDateRange = getShiftedDateRange(minTime, maxTime, scale);
|
||||
const newDateType = getDateTypeByDateRange(newDateRange.minTime, newDateRange.maxTime, firstDayOfWeek, scene);
|
||||
const newDateType = getDateTypeByDateRange(newDateRange.minTime, newDateRange.maxTime, firstDayOfWeek, fiscalYearStart, scene);
|
||||
|
||||
return {
|
||||
dateType: newDateType,
|
||||
@@ -562,13 +628,13 @@ export function getShiftedDateRangeAndDateType(minTime: number, maxTime: number,
|
||||
};
|
||||
}
|
||||
|
||||
export function getShiftedDateRangeAndDateTypeForBillingCycle(minTime: number, maxTime: number, scale: number, firstDayOfWeek: number, scene: number, statementDate: number | undefined | null): TimeRangeAndDateType | null {
|
||||
export function getShiftedDateRangeAndDateTypeForBillingCycle(minTime: number, maxTime: number, scale: number, firstDayOfWeek: number, fiscalYearStart: number, scene: number, statementDate: number | undefined | null): TimeRangeAndDateType | null {
|
||||
if (!statementDate || !DateRange.PreviousBillingCycle.isAvailableForScene(scene) || !DateRange.CurrentBillingCycle.isAvailableForScene(scene)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const previousBillingCycleRange = getDateRangeByBillingCycleDateType(DateRange.PreviousBillingCycle.type, firstDayOfWeek, statementDate);
|
||||
const currentBillingCycleRange = getDateRangeByBillingCycleDateType(DateRange.CurrentBillingCycle.type, firstDayOfWeek, statementDate);
|
||||
const previousBillingCycleRange = getDateRangeByBillingCycleDateType(DateRange.PreviousBillingCycle.type, firstDayOfWeek, fiscalYearStart, statementDate);
|
||||
const currentBillingCycleRange = getDateRangeByBillingCycleDateType(DateRange.CurrentBillingCycle.type, firstDayOfWeek, fiscalYearStart, statementDate);
|
||||
|
||||
if (previousBillingCycleRange && getUnixTimeBeforeUnixTime(previousBillingCycleRange.maxTime, 1, 'months') === maxTime && getUnixTimeBeforeUnixTime(previousBillingCycleRange.minTime, 1, 'months') === minTime && scale === 1) {
|
||||
return previousBillingCycleRange;
|
||||
@@ -583,7 +649,7 @@ export function getShiftedDateRangeAndDateTypeForBillingCycle(minTime: number, m
|
||||
return null;
|
||||
}
|
||||
|
||||
export function getDateTypeByDateRange(minTime: number, maxTime: number, firstDayOfWeek: number, scene: DateRangeScene): number {
|
||||
export function getDateTypeByDateRange(minTime: number, maxTime: number, firstDayOfWeek: number, fiscalYearStart: number, scene: DateRangeScene): number {
|
||||
const allDateRanges = DateRange.values();
|
||||
let newDateType = DateRange.Custom.type;
|
||||
|
||||
@@ -594,7 +660,7 @@ export function getDateTypeByDateRange(minTime: number, maxTime: number, firstDa
|
||||
continue;
|
||||
}
|
||||
|
||||
const range = getDateRangeByDateType(dateRange.type, firstDayOfWeek);
|
||||
const range = getDateRangeByDateType(dateRange.type, firstDayOfWeek, fiscalYearStart);
|
||||
|
||||
if (range && range.minTime === minTime && range.maxTime === maxTime) {
|
||||
newDateType = dateRange.type;
|
||||
@@ -605,13 +671,13 @@ export function getDateTypeByDateRange(minTime: number, maxTime: number, firstDa
|
||||
return newDateType;
|
||||
}
|
||||
|
||||
export function getDateTypeByBillingCycleDateRange(minTime: number, maxTime: number, firstDayOfWeek: number, scene: DateRangeScene, statementDate: number | undefined | null): number | null {
|
||||
export function getDateTypeByBillingCycleDateRange(minTime: number, maxTime: number, firstDayOfWeek: number, fiscalYearStart: number, scene: DateRangeScene, statementDate: number | undefined | null): number | null {
|
||||
if (!statementDate || !DateRange.PreviousBillingCycle.isAvailableForScene(scene) || !DateRange.CurrentBillingCycle.isAvailableForScene(scene)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const previousBillingCycleRange = getDateRangeByBillingCycleDateType(DateRange.PreviousBillingCycle.type, firstDayOfWeek, statementDate);
|
||||
const currentBillingCycleRange = getDateRangeByBillingCycleDateType(DateRange.CurrentBillingCycle.type, firstDayOfWeek, statementDate);
|
||||
const previousBillingCycleRange = getDateRangeByBillingCycleDateType(DateRange.PreviousBillingCycle.type, firstDayOfWeek, fiscalYearStart, statementDate);
|
||||
const currentBillingCycleRange = getDateRangeByBillingCycleDateType(DateRange.CurrentBillingCycle.type, firstDayOfWeek, fiscalYearStart, statementDate);
|
||||
|
||||
if (previousBillingCycleRange && previousBillingCycleRange.maxTime === maxTime && previousBillingCycleRange.minTime === minTime) {
|
||||
return previousBillingCycleRange.dateType;
|
||||
@@ -622,7 +688,7 @@ export function getDateTypeByBillingCycleDateRange(minTime: number, maxTime: num
|
||||
return null;
|
||||
}
|
||||
|
||||
export function getDateRangeByDateType(dateType: number | undefined, firstDayOfWeek: number): TimeRangeAndDateType | null {
|
||||
export function getDateRangeByDateType(dateType: number | undefined, firstDayOfWeek: number, fiscalYearStart: number): TimeRangeAndDateType | null {
|
||||
let maxTime = 0;
|
||||
let minTime = 0;
|
||||
|
||||
@@ -659,6 +725,12 @@ export function getDateRangeByDateType(dateType: number | undefined, firstDayOfW
|
||||
} else if (dateType === DateRange.LastYear.type) { // Last year
|
||||
maxTime = getUnixTimeBeforeUnixTime(getThisYearLastUnixTime(), 1, 'years');
|
||||
minTime = getUnixTimeBeforeUnixTime(getThisYearFirstUnixTime(), 1, 'years');
|
||||
} else if (dateType === DateRange.ThisFiscalYear.type) { // This fiscal year
|
||||
maxTime = getFiscalYearEndUnixTime(getTodayFirstUnixTime(), fiscalYearStart);
|
||||
minTime = getFiscalYearStartUnixTime(getTodayFirstUnixTime(), fiscalYearStart);
|
||||
} else if (dateType === DateRange.LastFiscalYear.type) { // Last fiscal year
|
||||
maxTime = getUnixTimeBeforeUnixTime(getFiscalYearEndUnixTime(getTodayFirstUnixTime(), fiscalYearStart), 1, 'years');
|
||||
minTime = getUnixTimeBeforeUnixTime(getFiscalYearStartUnixTime(getTodayFirstUnixTime(), fiscalYearStart), 1, 'years');
|
||||
} else if (dateType === DateRange.RecentTwelveMonths.type) { // Recent 12 months
|
||||
maxTime = getThisMonthLastUnixTime();
|
||||
minTime = getUnixTimeBeforeUnixTime(getThisMonthFirstUnixTime(), 11, 'months');
|
||||
@@ -688,7 +760,7 @@ export function getDateRangeByDateType(dateType: number | undefined, firstDayOfW
|
||||
};
|
||||
}
|
||||
|
||||
export function getDateRangeByBillingCycleDateType(dateType: number, firstDayOfWeek: number, statementDate: number | undefined | null): TimeRangeAndDateType | null {
|
||||
export function getDateRangeByBillingCycleDateType(dateType: number, firstDayOfWeek: number, fiscalYearStart: number, statementDate: number | undefined | null): TimeRangeAndDateType | null {
|
||||
let maxTime = 0;
|
||||
let minTime = 0;
|
||||
|
||||
@@ -710,9 +782,9 @@ export function getDateRangeByBillingCycleDateType(dateType: number, firstDayOfW
|
||||
let fallbackDateRange = null;
|
||||
|
||||
if (dateType === DateRange.CurrentBillingCycle.type) { // same as This Month
|
||||
fallbackDateRange = getDateRangeByDateType(DateRange.ThisMonth.type, firstDayOfWeek);
|
||||
fallbackDateRange = getDateRangeByDateType(DateRange.ThisMonth.type, firstDayOfWeek, fiscalYearStart);
|
||||
} else if (dateType === DateRange.PreviousBillingCycle.type) { // same as Last Month
|
||||
fallbackDateRange = getDateRangeByDateType(DateRange.LastMonth.type, firstDayOfWeek);
|
||||
fallbackDateRange = getDateRangeByDateType(DateRange.LastMonth.type, firstDayOfWeek, fiscalYearStart);
|
||||
}
|
||||
|
||||
if (fallbackDateRange) {
|
||||
@@ -775,8 +847,8 @@ export function getRecentDateRangeIndexByDateType(allRecentMonthDateRanges: Loca
|
||||
return -1;
|
||||
}
|
||||
|
||||
export function getRecentDateRangeIndex(allRecentMonthDateRanges: LocalizedRecentMonthDateRange[], dateType: number, minTime: number, maxTime: number, firstDayOfWeek: number): number {
|
||||
let dateRange = getDateRangeByDateType(dateType, firstDayOfWeek);
|
||||
export function getRecentDateRangeIndex(allRecentMonthDateRanges: LocalizedRecentMonthDateRange[], dateType: number, minTime: number, maxTime: number, firstDayOfWeek: number, fiscalYearStart: number): number {
|
||||
let dateRange = getDateRangeByDateType(dateType, firstDayOfWeek, fiscalYearStart);
|
||||
|
||||
if (dateRange && dateRange.dateType === DateRange.All.type) {
|
||||
return getRecentDateRangeIndexByDateType(allRecentMonthDateRanges, DateRange.All.type);
|
||||
@@ -805,18 +877,18 @@ export function getRecentDateRangeIndex(allRecentMonthDateRanges: LocalizedRecen
|
||||
return getRecentDateRangeIndexByDateType(allRecentMonthDateRanges, DateRange.Custom.type);
|
||||
}
|
||||
|
||||
export function getFullMonthDateRange(minTime: number, maxTime: number, firstDayOfWeek: number): TimeRangeAndDateType | null {
|
||||
export function getFullMonthDateRange(minTime: number, maxTime: number, firstDayOfWeek: number, fiscalYearStart: number): TimeRangeAndDateType | null {
|
||||
if (isDateRangeMatchOneMonth(minTime, maxTime)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!minTime) {
|
||||
return getDateRangeByDateType(DateRange.ThisMonth.type, firstDayOfWeek);
|
||||
return getDateRangeByDateType(DateRange.ThisMonth.type, firstDayOfWeek, fiscalYearStart);
|
||||
}
|
||||
|
||||
const monthFirstUnixTime = getMonthFirstUnixTimeBySpecifiedUnixTime(minTime);
|
||||
const monthLastUnixTime = getMonthLastUnixTimeBySpecifiedUnixTime(minTime);
|
||||
const dateType = getDateTypeByDateRange(monthFirstUnixTime, monthLastUnixTime, firstDayOfWeek, DateRangeScene.Normal);
|
||||
const dateType = getDateTypeByDateRange(monthFirstUnixTime, monthLastUnixTime, firstDayOfWeek, fiscalYearStart, DateRangeScene.Normal);
|
||||
|
||||
const dateRange: TimeRangeAndDateType = {
|
||||
dateType: dateType,
|
||||
@@ -937,3 +1009,115 @@ export function isDateRangeMatchOneMonth(minTime: number, maxTime: number): bool
|
||||
|
||||
return isDateRangeMatchFullMonths(minTime, maxTime);
|
||||
}
|
||||
|
||||
export function getFiscalYearFromUnixTime(unixTime: number, fiscalYearStart: number): number {
|
||||
const date = moment.unix(unixTime);
|
||||
|
||||
// For January 1 fiscal year start, fiscal year matches calendar year
|
||||
if (fiscalYearStart === 0x0101) {
|
||||
return date.year();
|
||||
}
|
||||
|
||||
// Get date components
|
||||
const month = date.month() + 1; // 1-index
|
||||
const day = date.date();
|
||||
const year = date.year();
|
||||
|
||||
const [fiscalYearStartMonth, fiscalYearStartDay] = FiscalYearStart.strictFromNumber(fiscalYearStart).values();
|
||||
|
||||
// For other fiscal year starts:
|
||||
// If input time comes before the fiscal year start day in the calendar year,
|
||||
// it belongs to the fiscal year that ends in the current calendar year
|
||||
if (month < fiscalYearStartMonth || (month === fiscalYearStartMonth && day < fiscalYearStartDay)) {
|
||||
return year;
|
||||
}
|
||||
|
||||
// If input time is on or after the fiscal year start day in the calendar year,
|
||||
// it belongs to the fiscal year that ends in the next calendar year
|
||||
return year + 1;
|
||||
}
|
||||
|
||||
export function getFiscalYearStartUnixTime(unixTime: number, fiscalYearStart: number): number {
|
||||
const date = moment.unix(unixTime);
|
||||
|
||||
// For January 1 fiscal year start, fiscal year start time is always January 1 in the input calendar year
|
||||
if (fiscalYearStart === 0x0101) {
|
||||
return moment().year(date.year()).month(0).date(1).hour(0).minute(0).second(0).millisecond(0).unix();
|
||||
}
|
||||
|
||||
const [fiscalYearStartMonth, fiscalYearStartDay] = FiscalYearStart.strictFromNumber(fiscalYearStart).values();
|
||||
const month = date.month() + 1; // 1-index
|
||||
const day = date.date();
|
||||
const year = date.year();
|
||||
|
||||
// For other fiscal year starts:
|
||||
// If input time comes before the fiscal year start day in the calendar year,
|
||||
// the relevant fiscal year has a start date in Calendar Year = Input Year, and end date in Calendar Year = Input Year + 1.
|
||||
// If input time comes on or after the fiscal year start day in the calendar year,
|
||||
// the relevant fiscal year has a start date in Calendar Year = Input Year - 1, and end date in Calendar Year = Input Year.
|
||||
let startYear = year - 1;
|
||||
if (month > fiscalYearStartMonth || (month === fiscalYearStartMonth && day >= fiscalYearStartDay)) {
|
||||
startYear = year;
|
||||
}
|
||||
|
||||
return moment().set({
|
||||
year: startYear,
|
||||
month: fiscalYearStartMonth - 1, // 0-index
|
||||
date: fiscalYearStartDay,
|
||||
hour: 0,
|
||||
minute: 0,
|
||||
second: 0,
|
||||
millisecond: 0,
|
||||
}).unix();
|
||||
}
|
||||
|
||||
export function getFiscalYearEndUnixTime(unixTime: number, fiscalYearStart: number): number {
|
||||
const fiscalYearStartTime = moment.unix(getFiscalYearStartUnixTime(unixTime, fiscalYearStart));
|
||||
return fiscalYearStartTime.add(1, 'year').subtract(1, 'second').unix();
|
||||
}
|
||||
|
||||
export function getCurrentFiscalYear(fiscalYearStart: number): number {
|
||||
const date = moment();
|
||||
return getFiscalYearFromUnixTime(date.unix(), fiscalYearStart);
|
||||
}
|
||||
|
||||
export function getFiscalYearTimeRangeFromUnixTime(unixTime: number, fiscalYearStart: number): FiscalYearUnixTime {
|
||||
const start = getFiscalYearStartUnixTime(unixTime, fiscalYearStart);
|
||||
const end = getFiscalYearEndUnixTime(unixTime, fiscalYearStart);
|
||||
return {
|
||||
year: getFiscalYearFromUnixTime(unixTime, fiscalYearStart),
|
||||
minUnixTime: start,
|
||||
maxUnixTime: end,
|
||||
};
|
||||
}
|
||||
|
||||
export function getFiscalYearTimeRangeFromYear(year: number, fiscalYearStart: number): FiscalYearUnixTime {
|
||||
const fiscalYear = year;
|
||||
const fiscalYearStartObj = FiscalYearStart.strictFromNumber(fiscalYearStart);
|
||||
|
||||
// For a specified fiscal year (e.g., 2023), the start date is in the previous calendar year
|
||||
// unless fiscal year starts on January 1
|
||||
const calendarStartYear = fiscalYearStart === 0x0101 ? fiscalYear : fiscalYear - 1;
|
||||
|
||||
// Create the timestamp for the start of the fiscal year
|
||||
const fiscalYearStartUnixTime = moment().set({
|
||||
year: calendarStartYear,
|
||||
month: fiscalYearStartObj.month - 1, // 0-index
|
||||
date: fiscalYearStartObj.day,
|
||||
hour: 0,
|
||||
minute: 0,
|
||||
second: 0,
|
||||
millisecond: 0,
|
||||
}).unix();
|
||||
|
||||
// Fiscal year end is one year after start minus 1 second
|
||||
const fiscalYearEndUnixTime = moment.unix(fiscalYearStartUnixTime).add(1, 'year').subtract(1, 'second').unix();
|
||||
|
||||
return {
|
||||
year: fiscalYear,
|
||||
minUnixTime: fiscalYearStartUnixTime,
|
||||
maxUnixTime: fiscalYearEndUnixTime,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { YearMonth, YearUnixTime, YearQuarterUnixTime, YearMonthUnixTime } from '@/core/datetime.ts';
|
||||
import type { FiscalYearUnixTime } from '@/core/fiscalyear.ts';
|
||||
import { ChartSortingType, ChartDateAggregationType } from '@/core/statistics.ts';
|
||||
import type {
|
||||
YearMonthItems,
|
||||
@@ -8,7 +9,8 @@ import type {
|
||||
import {
|
||||
getAllMonthsStartAndEndUnixTimes,
|
||||
getAllQuartersStartAndEndUnixTimes,
|
||||
getAllYearsStartAndEndUnixTimes
|
||||
getAllYearsStartAndEndUnixTimes,
|
||||
getAllFiscalYearsStartAndEndUnixTimes
|
||||
} from '@/lib/datetime.ts';
|
||||
|
||||
export function sortStatisticsItems<T extends SortableTransactionStatisticDataItem>(items: T[], sortingType: number): void {
|
||||
@@ -46,7 +48,7 @@ export function sortStatisticsItems<T extends SortableTransactionStatisticDataIt
|
||||
}
|
||||
}
|
||||
|
||||
export function getAllDateRanges<T extends YearMonth>(items: YearMonthItems<T>[], startYearMonth: YearMonth | string, endYearMonth: YearMonth | string, dateAggregationType: number): YearUnixTime[] | YearQuarterUnixTime[] | YearMonthUnixTime[] {
|
||||
export function getAllDateRanges<T extends YearMonth>(items: YearMonthItems<T>[], startYearMonth: YearMonth | string, endYearMonth: YearMonth | string, fiscalYearStart: number, dateAggregationType: number): YearUnixTime[] | YearQuarterUnixTime[] | YearMonthUnixTime[] | FiscalYearUnixTime[] {
|
||||
if ((!startYearMonth || !endYearMonth) && items && items.length) {
|
||||
let minYear = Number.MAX_SAFE_INTEGER, minMonth = Number.MAX_SAFE_INTEGER, maxYear = 0, maxMonth = 0;
|
||||
|
||||
@@ -78,6 +80,8 @@ export function getAllDateRanges<T extends YearMonth>(items: YearMonthItems<T>[]
|
||||
|
||||
if (dateAggregationType === ChartDateAggregationType.Year.type) {
|
||||
return getAllYearsStartAndEndUnixTimes(startYearMonth, endYearMonth);
|
||||
} else if (dateAggregationType === ChartDateAggregationType.FiscalYear.type) {
|
||||
return getAllFiscalYearsStartAndEndUnixTimes(startYearMonth, endYearMonth, fiscalYearStart);
|
||||
} else if (dateAggregationType === ChartDateAggregationType.Quarter.type) {
|
||||
return getAllQuartersStartAndEndUnixTimes(startYearMonth, endYearMonth);
|
||||
} else { // if (dateAggregationType === ChartDateAggregationType.Month.type) {
|
||||
|
||||
Reference in New Issue
Block a user