From 2c730b3e2510c0758591aaf2f3a1925aae6f8595 Mon Sep 17 00:00:00 2001 From: MaysWind Date: Mon, 9 Jun 2025 00:29:29 +0800 Subject: [PATCH] code refactor --- .../base/MonthRangeSelectionBase.ts | 69 +++++++++--- src/components/base/MonthSelectionBase.ts | 45 ++++++-- src/components/base/TrendsChartBase.ts | 6 +- .../desktop/MonthRangeSelectionDialog.vue | 7 +- .../desktop/MonthSelectionDialog.vue | 7 +- src/components/desktop/TrendsChart.vue | 14 +-- .../mobile/MonthRangeSelectionSheet.vue | 8 +- src/components/mobile/MonthSelectionSheet.vue | 6 +- src/components/mobile/TrendsBarChart.vue | 14 +-- src/core/datetime.ts | 29 +++-- src/core/fiscalyear.ts | 2 +- src/lib/datetime.ts | 104 ++++++++++-------- src/lib/statistics.ts | 12 +- src/models/transaction.ts | 14 +-- src/stores/statistics.ts | 4 +- src/stores/transaction.ts | 2 +- 16 files changed, 210 insertions(+), 133 deletions(-) diff --git a/src/components/base/MonthRangeSelectionBase.ts b/src/components/base/MonthRangeSelectionBase.ts index 2d4aa6e9..1fe06beb 100644 --- a/src/components/base/MonthRangeSelectionBase.ts +++ b/src/components/base/MonthRangeSelectionBase.ts @@ -1,11 +1,11 @@ import { ref, computed } from 'vue'; -import type { YearMonth } from '@/core/datetime.ts'; +import type { Year0BasedMonth } from '@/core/datetime.ts'; import { - getYearMonthObjectFromUnixTime, - getYearMonthObjectFromString, - getYearMonthStringFromObject, + getYear0BasedMonthObjectFromUnixTime, + getYear0BasedMonthObjectFromString, + getYearMonthStringFromYear0BasedMonthObject, getCurrentUnixTime, getCurrentYear, getThisYearFirstUnixTime, @@ -15,6 +15,11 @@ import { import { useI18n } from '@/locales/helpers.ts'; +export interface MonthSelectionValue { + year: number; + month: number; // 0-based month (0 = January, 11 = December) +} + export interface CommonMonthRangeSelectionProps { minTime?: string; maxTime?: string; @@ -23,12 +28,12 @@ export interface CommonMonthRangeSelectionProps { show: boolean; } -function getMonthRangeFromProps(props: CommonMonthRangeSelectionProps): { minDate: YearMonth; maxDate: YearMonth } { - let minDate: YearMonth = getYearMonthObjectFromUnixTime(getThisYearFirstUnixTime()); - let maxDate: YearMonth = getYearMonthObjectFromUnixTime(getCurrentUnixTime()); +function getMonthRangeFromProps(props: CommonMonthRangeSelectionProps): { minDate: MonthSelectionValue; maxDate: MonthSelectionValue } { + let minDate: Year0BasedMonth = getYear0BasedMonthObjectFromUnixTime(getThisYearFirstUnixTime()); + let maxDate: Year0BasedMonth = getYear0BasedMonthObjectFromUnixTime(getCurrentUnixTime()); if (props.minTime) { - const yearMonth = getYearMonthObjectFromString(props.minTime); + const yearMonth = getYear0BasedMonthObjectFromString(props.minTime); if (yearMonth) { minDate = yearMonth; @@ -36,7 +41,7 @@ function getMonthRangeFromProps(props: CommonMonthRangeSelectionProps): { minDat } if (props.maxTime) { - const yearMonth = getYearMonthObjectFromString(props.maxTime); + const yearMonth = getYear0BasedMonthObjectFromString(props.maxTime); if (yearMonth) { maxDate = yearMonth; @@ -44,8 +49,14 @@ function getMonthRangeFromProps(props: CommonMonthRangeSelectionProps): { minDat } return { - minDate, - maxDate + minDate: { + year: minDate.year, + month: minDate.month0base + }, + maxDate: { + year: maxDate.year, + month: maxDate.month0base + } }; } @@ -58,14 +69,33 @@ export function useMonthRangeSelectionBase(props: CommonMonthRangeSelectionProps getCurrentYear() + 1 ]); - const dateRange = ref([ + const dateRange = ref([ minDate, maxDate ]); const isYearFirst = computed(() => isLongDateMonthAfterYear()); - const beginDateTime = computed(() => formatUnixTimeToLongYearMonth(getYearMonthFirstUnixTime(dateRange.value[0]))); - const endDateTime = computed(() => formatUnixTimeToLongYearMonth(getYearMonthLastUnixTime(dateRange.value[1]))); + const beginDateTime = computed(() => formatUnixTimeToLongYearMonth(getYearMonthFirstUnixTime({ + year: dateRange.value[0].year, + month0base: dateRange.value[0].month + }))); + const endDateTime = computed(() => formatUnixTimeToLongYearMonth(getYearMonthLastUnixTime({ + year: dateRange.value[1].year, + month0base: dateRange.value[1].month + }))); + + function getMonthSelectionValue(yearMonth: string): MonthSelectionValue | null { + const yearMonthObj = getYear0BasedMonthObjectFromString(yearMonth); + + if (!yearMonthObj) { + return null; + } + + return { + year: yearMonthObj.year, + month: yearMonthObj.month0base + }; + } function getFinalMonthRange(): { minYearMonth: string, maxYearMonth: string } | null { if (!dateRange.value[0] || !dateRange.value[1]) { @@ -76,8 +106,14 @@ export function useMonthRangeSelectionBase(props: CommonMonthRangeSelectionProps throw new Error('Date is too early'); } - const minYearMonth = getYearMonthStringFromObject(dateRange.value[0]); - const maxYearMonth = getYearMonthStringFromObject(dateRange.value[1]); + const minYearMonth = getYearMonthStringFromYear0BasedMonthObject({ + year: dateRange.value[0].year, + month0base: dateRange.value[0].month + }); + const maxYearMonth = getYearMonthStringFromYear0BasedMonthObject({ + year: dateRange.value[1].year, + month0base: dateRange.value[1].month + }); return { minYearMonth, @@ -94,6 +130,7 @@ export function useMonthRangeSelectionBase(props: CommonMonthRangeSelectionProps beginDateTime, endDateTime, // functions + getMonthSelectionValue, getFinalMonthRange }; } diff --git a/src/components/base/MonthSelectionBase.ts b/src/components/base/MonthSelectionBase.ts index 81a65c36..6163c2fa 100644 --- a/src/components/base/MonthSelectionBase.ts +++ b/src/components/base/MonthSelectionBase.ts @@ -1,17 +1,22 @@ import { ref, computed } from 'vue'; -import type { YearMonth } from '@/core/datetime.ts'; +import type { Year0BasedMonth } from '@/core/datetime.ts'; import { - getYearMonthObjectFromUnixTime, - getYearMonthObjectFromString, - getYearMonthStringFromObject, + getYear0BasedMonthObjectFromUnixTime, + getYear0BasedMonthObjectFromString, + getYearMonthStringFromYear0BasedMonthObject, getCurrentYear, getThisMonthFirstUnixTime } from '@/lib/datetime.ts'; import { useI18n } from '@/locales/helpers.ts'; +export interface MonthSelectionValue { + year: number; + month: number; // 0-based month (0 = January, 11 = December) +} + export interface CommonMonthSelectionProps { modelValue?: string; title?: string; @@ -19,18 +24,21 @@ export interface CommonMonthSelectionProps { show: boolean; } -function getYearMonthValueFromProps(props: CommonMonthSelectionProps): YearMonth { - let value: YearMonth = getYearMonthObjectFromUnixTime(getThisMonthFirstUnixTime()); +function getYearMonthValueFromProps(props: CommonMonthSelectionProps): MonthSelectionValue { + let value: Year0BasedMonth = getYear0BasedMonthObjectFromUnixTime(getThisMonthFirstUnixTime()); if (props.modelValue) { - const yearMonth = getYearMonthObjectFromString(props.modelValue); + const yearMonth = getYear0BasedMonthObjectFromString(props.modelValue); if (yearMonth) { value = yearMonth; } } - return value; + return { + year: value.year, + month: value.month0base + }; } export function useMonthSelectionBase(props: CommonMonthSelectionProps) { @@ -41,10 +49,23 @@ export function useMonthSelectionBase(props: CommonMonthSelectionProps) { getCurrentYear() + 1 ]); - const monthValue = ref(getYearMonthValueFromProps(props)); + const monthValue = ref(getYearMonthValueFromProps(props)); const isYearFirst = computed(() => isLongDateMonthAfterYear()); + function getMonthSelectionValue(yearMonth: string): MonthSelectionValue | null { + const yearMonthObj = getYear0BasedMonthObjectFromString(yearMonth); + + if (!yearMonthObj) { + return null; + } + + return { + year: yearMonthObj.year, + month: yearMonthObj.month0base + }; + } + function getTextualYearMonth(): string | null { if (!monthValue.value) { return null; @@ -54,7 +75,10 @@ export function useMonthSelectionBase(props: CommonMonthSelectionProps) { throw new Error('Date is too early'); } - return getYearMonthStringFromObject(monthValue.value); + return getYearMonthStringFromYear0BasedMonthObject({ + year: monthValue.value.year, + month0base: monthValue.value.month + }); } return { @@ -64,6 +88,7 @@ export function useMonthSelectionBase(props: CommonMonthSelectionProps) { // computed states isYearFirst, // functions + getMonthSelectionValue, getTextualYearMonth }; } diff --git a/src/components/base/TrendsChartBase.ts b/src/components/base/TrendsChartBase.ts index 19db44a4..49f7f6a2 100644 --- a/src/components/base/TrendsChartBase.ts +++ b/src/components/base/TrendsChartBase.ts @@ -3,7 +3,7 @@ import { computed } from 'vue'; import { useI18n } from '@/locales/helpers.ts'; import type { - YearMonth, + Year1BasedMonth, TimeRangeAndDateType, YearUnixTime, YearQuarterUnixTime, @@ -16,7 +16,7 @@ import type { YearMonthItems } from '@/models/transaction.ts'; import { getAllDateRanges } from '@/lib/statistics.ts'; -export interface CommonTrendsChartProps { +export interface CommonTrendsChartProps { items: YearMonthItems[]; startYearMonth: string; endYearMonth: string; @@ -39,7 +39,7 @@ export interface TrendsBarChartClickEvent { dateRange: TimeRangeAndDateType; } -export function useTrendsChartBase(props: CommonTrendsChartProps) { +export function useTrendsChartBase(props: CommonTrendsChartProps) { const { tt } = useI18n(); const allDateRanges = computed(() => getAllDateRanges(props.items, props.startYearMonth, props.endYearMonth, props.fiscalYearStart, props.dateAggregationType)); diff --git a/src/components/desktop/MonthRangeSelectionDialog.vue b/src/components/desktop/MonthRangeSelectionDialog.vue index 72a81f87..f32bd4f7 100644 --- a/src/components/desktop/MonthRangeSelectionDialog.vue +++ b/src/components/desktop/MonthRangeSelectionDialog.vue @@ -71,7 +71,6 @@ import { useI18n } from '@/locales/helpers.ts'; import { type CommonMonthRangeSelectionProps, useMonthRangeSelectionBase } from '@/components/base/MonthRangeSelectionBase.ts'; import { ThemeType } from '@/core/theme.ts'; -import { getYearMonthObjectFromString } from '@/lib/datetime.ts'; interface DesktopMonthRangeSelectionProps extends CommonMonthRangeSelectionProps { persistent?: boolean; @@ -87,7 +86,7 @@ const emit = defineEmits<{ const theme = useTheme(); const { tt, getMonthShortName } = useI18n(); -const { yearRange, dateRange, isYearFirst, beginDateTime, endDateTime, getFinalMonthRange } = useMonthRangeSelectionBase(props); +const { yearRange, dateRange, isYearFirst, beginDateTime, endDateTime, getMonthSelectionValue, getFinalMonthRange } = useMonthRangeSelectionBase(props); const isDarkMode = computed(() => theme.global.name.value === ThemeType.Dark); const showState = computed({ @@ -117,7 +116,7 @@ function cancel(): void { watch(() => props.minTime, (newValue) => { if (newValue) { - const yearMonth = getYearMonthObjectFromString(newValue); + const yearMonth = getMonthSelectionValue(newValue); if (yearMonth) { dateRange.value[0] = yearMonth; @@ -127,7 +126,7 @@ watch(() => props.minTime, (newValue) => { watch(() => props.maxTime, (newValue) => { if (newValue) { - const yearMonth = getYearMonthObjectFromString(newValue); + const yearMonth = getMonthSelectionValue(newValue); if (yearMonth) { dateRange.value[1] = yearMonth; diff --git a/src/components/desktop/MonthSelectionDialog.vue b/src/components/desktop/MonthSelectionDialog.vue index 96495bc4..9dfdfbe7 100644 --- a/src/components/desktop/MonthSelectionDialog.vue +++ b/src/components/desktop/MonthSelectionDialog.vue @@ -50,7 +50,6 @@ import { useI18n } from '@/locales/helpers.ts'; import { type CommonMonthSelectionProps, useMonthSelectionBase } from '@/components/base/MonthSelectionBase.ts'; import { ThemeType } from '@/core/theme.ts'; -import { getYearMonthObjectFromString } from '@/lib/datetime.ts'; interface DesktopMonthSelectionProps extends CommonMonthSelectionProps { persistent?: boolean; @@ -66,7 +65,7 @@ const emit = defineEmits<{ const theme = useTheme(); const { tt, getMonthShortName } = useI18n(); -const { yearRange, monthValue, isYearFirst, getTextualYearMonth } = useMonthSelectionBase(props); +const { yearRange, monthValue, isYearFirst, getMonthSelectionValue, getTextualYearMonth } = useMonthSelectionBase(props); const isDarkMode = computed(() => theme.global.name.value === ThemeType.Dark); const showState = computed({ @@ -96,7 +95,7 @@ function cancel(): void { watch(() => props.modelValue, (newValue) => { if (newValue) { - const yearMonth = getYearMonthObjectFromString(newValue); + const yearMonth = getMonthSelectionValue(newValue); if (yearMonth) { monthValue.value = yearMonth; @@ -106,7 +105,7 @@ watch(() => props.modelValue, (newValue) => { watch(() => props.show, (newValue) => { if (newValue && props.modelValue) { - const yearMonth = getYearMonthObjectFromString(props.modelValue); + const yearMonth = getMonthSelectionValue(props.modelValue); if (yearMonth) { monthValue.value = yearMonth; diff --git a/src/components/desktop/TrendsChart.vue b/src/components/desktop/TrendsChart.vue index 81983628..45c9fe0f 100644 --- a/src/components/desktop/TrendsChart.vue +++ b/src/components/desktop/TrendsChart.vue @@ -14,7 +14,7 @@ import { type CommonTrendsChartProps, type TrendsBarChartClickEvent, useTrendsCh import { useUserStore } from '@/stores/user.ts'; -import { type YearMonth, DateRangeScene } from '@/core/datetime.ts'; +import { type Year1BasedMonth, DateRangeScene } from '@/core/datetime.ts'; import type { ColorValue } from '@/core/color.ts'; import { ThemeType } from '@/core/theme.ts'; import { TrendChartType, ChartDateAggregationType } from '@/core/statistics.ts'; @@ -35,7 +35,7 @@ import { sortStatisticsItems } from '@/lib/statistics.ts'; -interface DesktopTrendsChartProps extends CommonTrendsChartProps { +interface DesktopTrendsChartProps extends CommonTrendsChartProps { skeleton?: boolean; type: number; showValue?: boolean; @@ -155,14 +155,14 @@ const allSeries = computed(() => { dateRangeKey = dataItem.year.toString(); } else if (props.dateAggregationType === ChartDateAggregationType.FiscalYear.type) { const fiscalYear = getFiscalYearFromUnixTime( - getYearMonthFirstUnixTime({ year: dataItem.year, month: dataItem.month - 1 }), + getYearMonthFirstUnixTime({ year: dataItem.year, month1base: dataItem.month1base }), props.fiscalYearStart ); dateRangeKey = fiscalYear.toString(); } else if (props.dateAggregationType === ChartDateAggregationType.Quarter.type) { - dateRangeKey = `${dataItem.year}-${Math.floor((dataItem.month - 1) / 3) + 1}`; + dateRangeKey = `${dataItem.year}-${Math.floor((dataItem.month1base - 1) / 3) + 1}`; } else { // if (props.dateAggregationType === ChartDateAggregationType.Month.type) { - dateRangeKey = `${dataItem.year}-${dataItem.month}`; + dateRangeKey = `${dataItem.year}-${dataItem.month1base}`; } const dataItems = dateRangeAmountMap[dateRangeKey] || []; @@ -181,8 +181,8 @@ const allSeries = computed(() => { dateRangeKey = dateRange.year.toString(); } else if (props.dateAggregationType === ChartDateAggregationType.Quarter.type && 'quarter' in dateRange) { dateRangeKey = `${dateRange.year}-${dateRange.quarter}`; - } else if (props.dateAggregationType === ChartDateAggregationType.Month.type && 'month' in dateRange) { - dateRangeKey = `${dateRange.year}-${dateRange.month + 1}`; + } else if (props.dateAggregationType === ChartDateAggregationType.Month.type && 'month0base' in dateRange) { + dateRangeKey = `${dateRange.year}-${dateRange.month0base + 1}`; } let amount = 0; diff --git a/src/components/mobile/MonthRangeSelectionSheet.vue b/src/components/mobile/MonthRangeSelectionSheet.vue index b685fbae..81920145 100644 --- a/src/components/mobile/MonthRangeSelectionSheet.vue +++ b/src/components/mobile/MonthRangeSelectionSheet.vue @@ -52,8 +52,6 @@ import { type CommonMonthRangeSelectionProps, useMonthRangeSelectionBase } from import { useEnvironmentsStore } from '@/stores/environment.ts'; -import { getYearMonthObjectFromString } from '@/lib/datetime.ts'; - const props = defineProps(); const emit = defineEmits<{ (e: 'update:show', value: boolean): void; @@ -62,7 +60,7 @@ const emit = defineEmits<{ const { tt, getMonthShortName } = useI18n(); const { showToast } = useI18nUIComponents(); -const { yearRange, dateRange, isYearFirst, beginDateTime, endDateTime, getFinalMonthRange } = useMonthRangeSelectionBase(props); +const { yearRange, dateRange, isYearFirst, beginDateTime, endDateTime, getMonthSelectionValue, getFinalMonthRange } = useMonthRangeSelectionBase(props); const environmentsStore = useEnvironmentsStore(); @@ -90,7 +88,7 @@ function cancel(): void { function onSheetOpen(): void { if (props.minTime) { - const yearMonth = getYearMonthObjectFromString(props.minTime); + const yearMonth = getMonthSelectionValue(props.minTime); if (yearMonth) { dateRange.value[0] = yearMonth; @@ -98,7 +96,7 @@ function onSheetOpen(): void { } if (props.maxTime) { - const yearMonth = getYearMonthObjectFromString(props.maxTime); + const yearMonth = getMonthSelectionValue(props.maxTime); if (yearMonth) { dateRange.value[1] = yearMonth; diff --git a/src/components/mobile/MonthSelectionSheet.vue b/src/components/mobile/MonthSelectionSheet.vue index 63d6b527..b9a22588 100644 --- a/src/components/mobile/MonthSelectionSheet.vue +++ b/src/components/mobile/MonthSelectionSheet.vue @@ -46,8 +46,6 @@ import { type CommonMonthSelectionProps, useMonthSelectionBase } from '@/compone import { useEnvironmentsStore } from '@/stores/environment.ts'; -import { getYearMonthObjectFromString } from '@/lib/datetime.ts'; - const props = defineProps(); const emit = defineEmits<{ (e: 'update:modelValue', value: string): void; @@ -56,7 +54,7 @@ const emit = defineEmits<{ const { tt, getMonthShortName } = useI18n(); const { showToast } = useI18nUIComponents(); -const { yearRange, monthValue, isYearFirst, getTextualYearMonth } = useMonthSelectionBase(props); +const { yearRange, monthValue, isYearFirst, getMonthSelectionValue, getTextualYearMonth } = useMonthSelectionBase(props); const environmentsStore = useEnvironmentsStore(); @@ -84,7 +82,7 @@ function cancel(): void { function onSheetOpen(): void { if (props.modelValue) { - const yearMonth = getYearMonthObjectFromString(props.modelValue); + const yearMonth = getMonthSelectionValue(props.modelValue); if (yearMonth) { monthValue.value = yearMonth; diff --git a/src/components/mobile/TrendsBarChart.vue b/src/components/mobile/TrendsBarChart.vue index f4d5181d..375a956a 100644 --- a/src/components/mobile/TrendsBarChart.vue +++ b/src/components/mobile/TrendsBarChart.vue @@ -95,7 +95,7 @@ import { type CommonTrendsChartProps, type TrendsBarChartClickEvent, useTrendsCh import { useUserStore } from '@/stores/user.ts'; -import { type YearMonth, type UnixTimeRange, DateRangeScene } from '@/core/datetime.ts'; +import { type Year1BasedMonth, type UnixTimeRange, DateRangeScene } from '@/core/datetime.ts'; import { ChartDateAggregationType } from '@/core/statistics.ts'; import { DEFAULT_CHART_COLORS } from '@/consts/color.ts'; import type { YearMonthDataItem, SortableTransactionStatisticDataItem } from '@/models/transaction.ts'; @@ -138,7 +138,7 @@ interface TrendsBarChartData { readonly legends: TrendsBarChartLegend[]; } -interface MobileTrendsChartProps extends CommonTrendsChartProps { +interface MobileTrendsChartProps extends CommonTrendsChartProps { loading?: boolean; } @@ -191,14 +191,14 @@ const allDisplayDataItems = computed(() => { dateRangeKey = dataItem.year.toString(); } else if (props.dateAggregationType === ChartDateAggregationType.FiscalYear.type) { const fiscalYear = getFiscalYearFromUnixTime( - getYearMonthFirstUnixTime({ year: dataItem.year, month: dataItem.month - 1 }), + getYearMonthFirstUnixTime({ year: dataItem.year, month1base: dataItem.month1base }), props.fiscalYearStart ); dateRangeKey = fiscalYear.toString(); } else if (props.dateAggregationType === ChartDateAggregationType.Quarter.type) { - dateRangeKey = `${dataItem.year}-${Math.floor((dataItem.month - 1) / 3) + 1}`; + dateRangeKey = `${dataItem.year}-${Math.floor((dataItem.month1base - 1) / 3) + 1}`; } else { // if (props.dateAggregationType === ChartDateAggregationType.Month.type) { - dateRangeKey = `${dataItem.year}-${dataItem.month}`; + dateRangeKey = `${dataItem.year}-${dataItem.month1base}`; } if (dateRangeItemMap[dateRangeKey]) { @@ -229,8 +229,8 @@ const allDisplayDataItems = computed(() => { dateRangeKey = dateRange.year.toString(); } else if (props.dateAggregationType === ChartDateAggregationType.Quarter.type && 'quarter' in dateRange) { dateRangeKey = `${dateRange.year}-${dateRange.quarter}`; - } else if (props.dateAggregationType === ChartDateAggregationType.Month.type && 'month' in dateRange) { - dateRangeKey = `${dateRange.year}-${dateRange.month + 1}`; + } else if (props.dateAggregationType === ChartDateAggregationType.Month.type && 'month0base' in dateRange) { + dateRangeKey = `${dateRange.year}-${dateRange.month0base + 1}`; } let displayDateRange = ''; diff --git a/src/core/datetime.ts b/src/core/datetime.ts index dc623e2f..858585ca 100644 --- a/src/core/datetime.ts +++ b/src/core/datetime.ts @@ -5,14 +5,19 @@ export interface YearQuarter { readonly quarter: number; } -export interface YearMonth { +export interface Year0BasedMonth { readonly year: number; - readonly month: number; + readonly month0base: number; +} + +export interface Year1BasedMonth { + readonly year: number; + readonly month1base: number; } export interface YearMonthRange { - readonly startYearMonth: YearMonth; - readonly endYearMonth: YearMonth; + readonly startYearMonth: Year0BasedMonth; + readonly endYearMonth: Year0BasedMonth; } export interface TimeRange { @@ -49,7 +54,7 @@ export interface RecentMonthDateRange { readonly minTime: number; readonly maxTime: number; readonly year: number; - readonly month: number; + readonly month: number; // 1-based (1 = January, 12 = December) } export interface PresetDateRange { @@ -118,21 +123,21 @@ export class YearQuarterUnixTime implements YearQuarter, UnixTimeRange { } } -export class YearMonthUnixTime implements YearMonth, UnixTimeRange { +export class YearMonthUnixTime implements Year0BasedMonth, UnixTimeRange { public readonly year: number; - public readonly month: number; + public readonly month0base: number; public readonly minUnixTime: number; public readonly maxUnixTime: number; - private constructor(year: number, month: number, minUnixTime: number, maxUnixTime: number) { + private constructor(year: number, month0base: number, minUnixTime: number, maxUnixTime: number) { this.year = year; - this.month = month; + this.month0base = month0base; this.minUnixTime = minUnixTime; this.maxUnixTime = maxUnixTime; } - public static of(yearMonth: YearMonth, minUnixTime: number, maxUnixTime: number): YearMonthUnixTime { - return new YearMonthUnixTime(yearMonth.year, yearMonth.month, minUnixTime, maxUnixTime); + public static of(yearMonth: Year0BasedMonth, minUnixTime: number, maxUnixTime: number): YearMonthUnixTime { + return new YearMonthUnixTime(yearMonth.year, yearMonth.month0base, minUnixTime, maxUnixTime); } } @@ -152,7 +157,7 @@ export class Month { public static readonly November = new Month(11, 'November'); public static readonly December = new Month(12, 'December'); - public readonly month: number; + public readonly month: number; // 1-based (1 = January, 12 = December) public readonly name: string; private constructor(month: number, name: string) { diff --git a/src/core/fiscalyear.ts b/src/core/fiscalyear.ts index 3f78bdb8..2f4e871d 100644 --- a/src/core/fiscalyear.ts +++ b/src/core/fiscalyear.ts @@ -19,7 +19,7 @@ export class FiscalYearStart { 31 // December ]; - public readonly month: number; + public readonly month: number; // 1-based (1 = January, 12 = December) public readonly day: number; public readonly value: number; diff --git a/src/lib/datetime.ts b/src/lib/datetime.ts index 717e9388..d603a865 100644 --- a/src/lib/datetime.ts +++ b/src/lib/datetime.ts @@ -4,7 +4,8 @@ import { type unitOfTime } from 'moment/moment'; import { type YearUnixTime, type YearQuarter, - type YearMonth, + type Year0BasedMonth, + type Year1BasedMonth, type YearMonthRange, type TimeRange, type TimeRangeAndDateType, @@ -34,24 +35,24 @@ import { type SupportedDate = Date | moment.Moment; -export function isYearMonthValid(year: number, month: number): boolean { - if (!isNumber(year) || !isNumber(month)) { +export function isYear0BasedMonthValid(year: number, month0base: number): boolean { + if (!isNumber(year) || !isNumber(month0base)) { return false; } - return year > 0 && month >= 0 && month <= 11; + return year > 0 && month0base >= 0 && month0base <= 11; } -export function getYearMonthObjectFromUnixTime(unixTime: number): YearMonth { +export function getYear0BasedMonthObjectFromUnixTime(unixTime: number): Year0BasedMonth { const datetime = moment.unix(unixTime); return { year: datetime.year(), - month: datetime.month() + month0base: datetime.month() }; } -export function getYearMonthObjectFromString(yearMonth: string): YearMonth | null { +export function getYear0BasedMonthObjectFromString(yearMonth: string): Year0BasedMonth | null { if (!isString(yearMonth)) { return null; } @@ -63,24 +64,24 @@ export function getYearMonthObjectFromString(yearMonth: string): YearMonth | nul } const year = parseInt(items[0]); - const month = parseInt(items[1]) - 1; + const month0base = parseInt(items[1]) - 1; - if (!isYearMonthValid(year, month)) { + if (!isYear0BasedMonthValid(year, month0base)) { return null; } return { year: year, - month: month + month0base: month0base }; } -export function getYearMonthStringFromObject(yearMonth: YearMonth | null): string { - if (!yearMonth || !isYearMonthValid(yearMonth.year, yearMonth.month)) { +export function getYearMonthStringFromYear0BasedMonthObject(yearMonth: Year0BasedMonth | null): string { + if (!yearMonth || !isYear0BasedMonthValid(yearMonth.year, yearMonth.month0base)) { return ''; } - return `${yearMonth.year}-${yearMonth.month + 1}`; + return `${yearMonth.year}-${yearMonth.month0base + 1}`; } export function getTwoDigitsString(value: number): string { @@ -375,40 +376,55 @@ export function getQuarterLastUnixTime(yearQuarter: YearQuarter): number { return moment.unix(getQuarterFirstUnixTime(yearQuarter)).add(3, 'months').subtract(1, 'seconds').unix(); } -export function getYearMonthFirstUnixTime(yearMonth: YearMonth | string): number { - let yearMonthObj: YearMonth | null = null; +export function getYearMonthFirstUnixTime(yearMonth: Year0BasedMonth | Year1BasedMonth | string): number { + let yearMonthObj: Year0BasedMonth | null = null; if (isString(yearMonth)) { - yearMonthObj = getYearMonthObjectFromString(yearMonth); - } else if (isObject(yearMonth) && isYearMonthValid(yearMonth.year, yearMonth.month)) { + yearMonthObj = getYear0BasedMonthObjectFromString(yearMonth); + } else if (isObject(yearMonth) && ('month0base' in yearMonth) && isYear0BasedMonthValid(yearMonth.year, yearMonth.month0base)) { yearMonthObj = yearMonth; + } else if (isObject(yearMonth) && ('month1base' in yearMonth) && isYear0BasedMonthValid(yearMonth.year, yearMonth.month1base - 1)) { + yearMonthObj = { + year: yearMonth.year, + month0base: yearMonth.month1base - 1 + }; } if (!yearMonthObj) { return 0; } - return moment().set({ year: yearMonthObj.year, month: yearMonthObj.month, date: 1, hour: 0, minute: 0, second: 0, millisecond: 0 }).unix(); + return moment().set({ year: yearMonthObj.year, month: yearMonthObj.month0base, date: 1, hour: 0, minute: 0, second: 0, millisecond: 0 }).unix(); } -export function getYearMonthLastUnixTime(yearMonth: YearMonth | string): number { +export function getYearMonthLastUnixTime(yearMonth: Year0BasedMonth | Year1BasedMonth | string): number { return moment.unix(getYearMonthFirstUnixTime(yearMonth)).add(1, 'months').subtract(1, 'seconds').unix(); } -export function getStartEndYearMonthRange(startYearMonth: YearMonth | string, endYearMonth: YearMonth | string): YearMonthRange | null { - let startYearMonthObj: YearMonth | null = null; - let endYearMonthObj: YearMonth | null = null; +export function getStartEndYearMonthRange(startYearMonth: Year0BasedMonth | Year1BasedMonth | string, endYearMonth: Year0BasedMonth | Year1BasedMonth | string): YearMonthRange | null { + let startYearMonthObj: Year0BasedMonth | null = null; + let endYearMonthObj: Year0BasedMonth | null = null; if (isString(startYearMonth)) { - startYearMonthObj = getYearMonthObjectFromString(startYearMonth); - } else if (isObject(startYearMonth)) { + startYearMonthObj = getYear0BasedMonthObjectFromString(startYearMonth); + } else if (isObject(startYearMonth) && ('month0base' in startYearMonth)) { startYearMonthObj = startYearMonth; + } else if (isObject(startYearMonth) && ('month1base' in startYearMonth)) { + startYearMonthObj = { + year: startYearMonth.year, + month0base: startYearMonth.month1base - 1 + }; } if (isString(endYearMonth)) { - endYearMonthObj = getYearMonthObjectFromString(endYearMonth); - } else { + endYearMonthObj = getYear0BasedMonthObjectFromString(endYearMonth); + } else if (isObject(endYearMonth) && ('month0base' in endYearMonth)) { endYearMonthObj = endYearMonth; + } else if (isObject(endYearMonth) && ('month1base' in endYearMonth)) { + endYearMonthObj = { + year: endYearMonth.year, + month0base: endYearMonth.month1base - 1 + }; } if (!startYearMonthObj || !endYearMonthObj) { @@ -421,7 +437,7 @@ export function getStartEndYearMonthRange(startYearMonth: YearMonth | string, en }; } -export function getAllYearsStartAndEndUnixTimes(startYearMonth: YearMonth | string, endYearMonth: YearMonth | string): YearUnixTime[] { +export function getAllYearsStartAndEndUnixTimes(startYearMonth: Year0BasedMonth | Year1BasedMonth | string, endYearMonth: Year0BasedMonth | Year1BasedMonth | string): YearUnixTime[] { const allYearTimes: YearUnixTime[] = []; const range = getStartEndYearMonthRange(startYearMonth, endYearMonth); @@ -442,7 +458,7 @@ export function getAllYearsStartAndEndUnixTimes(startYearMonth: YearMonth | stri return allYearTimes; } -export function getAllFiscalYearsStartAndEndUnixTimes(startYearMonth: YearMonth | string, endYearMonth: YearMonth | string, fiscalYearStartValue: number): FiscalYearUnixTime[] { +export function getAllFiscalYearsStartAndEndUnixTimes(startYearMonth: Year0BasedMonth | Year1BasedMonth | string, endYearMonth: Year0BasedMonth | Year1BasedMonth | string, fiscalYearStartValue: 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 @@ -468,7 +484,7 @@ export function getAllFiscalYearsStartAndEndUnixTimes(startYearMonth: YearMonth // 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: fiscalYearStart.month - 1 }); + const thisYearMonthUnixTime = getYearMonthFirstUnixTime({ year: year, month1base: fiscalYearStart.month }); const fiscalStartTime = getFiscalYearStartUnixTime(thisYearMonthUnixTime, fiscalYearStart.value); const fiscalEndTime = getFiscalYearEndUnixTime(thisYearMonthUnixTime, fiscalYearStart.value); @@ -504,7 +520,7 @@ export function getAllFiscalYearsStartAndEndUnixTimes(startYearMonth: YearMonth return allFiscalYearTimes; } -export function getAllQuartersStartAndEndUnixTimes(startYearMonth: YearMonth | string, endYearMonth: YearMonth | string): YearQuarterUnixTime[] { +export function getAllQuartersStartAndEndUnixTimes(startYearMonth: Year0BasedMonth | Year1BasedMonth | string, endYearMonth: Year0BasedMonth | Year1BasedMonth | string): YearQuarterUnixTime[] { const allYearQuarterTimes: YearQuarterUnixTime[] = []; const range = getStartEndYearMonthRange(startYearMonth, endYearMonth); @@ -512,10 +528,10 @@ export function getAllQuartersStartAndEndUnixTimes(startYearMonth: YearMonth | s return allYearQuarterTimes; } - for (let year = range.startYearMonth.year, month = range.startYearMonth.month; year < range.endYearMonth.year || (year === range.endYearMonth.year && ((month / 3) <= (range.endYearMonth.month / 3))); ) { + for (let year = range.startYearMonth.year, month0base = range.startYearMonth.month0base; year < range.endYearMonth.year || (year === range.endYearMonth.year && ((month0base / 3) <= (range.endYearMonth.month0base / 3))); ) { const yearQuarter: YearQuarter = { year: year, - quarter: Math.floor((month / 3)) + 1 + quarter: Math.floor((month0base / 3)) + 1 }; const minUnixTime = getQuarterFirstUnixTime(yearQuarter); @@ -523,22 +539,22 @@ export function getAllQuartersStartAndEndUnixTimes(startYearMonth: YearMonth | s allYearQuarterTimes.push(YearQuarterUnixTime.of(yearQuarter, minUnixTime, maxUnixTime)); - if (year === range.endYearMonth.year && month >= range.endYearMonth.month) { + if (year === range.endYearMonth.year && month0base >= range.endYearMonth.month0base) { break; } - if (month >= 9) { + if (month0base >= 9) { year++; - month = 0; + month0base = 0; } else { - month += 3; + month0base += 3; } } return allYearQuarterTimes; } -export function getAllMonthsStartAndEndUnixTimes(startYearMonth: YearMonth | string, endYearMonth: YearMonth | string): YearMonthUnixTime[] { +export function getAllMonthsStartAndEndUnixTimes(startYearMonth: Year0BasedMonth | Year1BasedMonth | string, endYearMonth: Year0BasedMonth | Year1BasedMonth | string): YearMonthUnixTime[] { const allYearMonthTimes: YearMonthUnixTime[] = []; const range = getStartEndYearMonthRange(startYearMonth, endYearMonth); @@ -546,10 +562,10 @@ export function getAllMonthsStartAndEndUnixTimes(startYearMonth: YearMonth | str return allYearMonthTimes; } - for (let year = range.startYearMonth.year, month = range.startYearMonth.month; year <= range.endYearMonth.year || month <= range.endYearMonth.month; ) { - const yearMonth: YearMonth = { + for (let year = range.startYearMonth.year, month0base = range.startYearMonth.month0base; year <= range.endYearMonth.year || month0base <= range.endYearMonth.month0base; ) { + const yearMonth: Year0BasedMonth = { year: year, - month: month + month0base: month0base }; const minUnixTime = getYearMonthFirstUnixTime(yearMonth); @@ -557,15 +573,15 @@ export function getAllMonthsStartAndEndUnixTimes(startYearMonth: YearMonth | str allYearMonthTimes.push(YearMonthUnixTime.of(yearMonth, minUnixTime, maxUnixTime)); - if (year === range.endYearMonth.year && month === range.endYearMonth.month) { + if (year === range.endYearMonth.year && month0base === range.endYearMonth.month0base) { break; } - if (month >= 11) { + if (month0base >= 11) { year++; - month = 0; + month0base = 0; } else { - month++; + month0base++; } } diff --git a/src/lib/statistics.ts b/src/lib/statistics.ts index dddc547e..5e2be9d1 100644 --- a/src/lib/statistics.ts +++ b/src/lib/statistics.ts @@ -1,4 +1,4 @@ -import type { YearMonth, YearUnixTime, YearQuarterUnixTime, YearMonthUnixTime } from '@/core/datetime.ts'; +import type { Year1BasedMonth, YearUnixTime, YearQuarterUnixTime, YearMonthUnixTime } from '@/core/datetime.ts'; import type { FiscalYearUnixTime } from '@/core/fiscalyear.ts'; import { ChartSortingType, ChartDateAggregationType } from '@/core/statistics.ts'; import type { @@ -48,7 +48,7 @@ export function sortStatisticsItems(items: YearMonthItems[], startYearMonth: YearMonth | string, endYearMonth: YearMonth | string, fiscalYearStart: number, dateAggregationType: number): YearUnixTime[] | FiscalYearUnixTime[] | YearQuarterUnixTime[] | YearMonthUnixTime[] { +export function getAllDateRanges(items: YearMonthItems[], startYearMonth: Year1BasedMonth | string, endYearMonth: Year1BasedMonth | string, fiscalYearStart: number, dateAggregationType: number): YearUnixTime[] | FiscalYearUnixTime[] | YearQuarterUnixTime[] | YearMonthUnixTime[] { if ((!startYearMonth || !endYearMonth) && items && items.length) { let minYear = Number.MAX_SAFE_INTEGER, minMonth = Number.MAX_SAFE_INTEGER, maxYear = 0, maxMonth = 0; @@ -58,14 +58,14 @@ export function getAllDateRanges(items: YearMonthItems[] for (let j = 0; j < item.items.length; j++) { const dataItem = item.items[j]; - if (dataItem.year < minYear || (dataItem.year === minYear && dataItem.month < minMonth)) { + if (dataItem.year < minYear || (dataItem.year === minYear && dataItem.month1base < minMonth)) { minYear = dataItem.year; - minMonth = dataItem.month; + minMonth = dataItem.month1base; } - if (dataItem.year > maxYear || (dataItem.year === maxYear && dataItem.month > maxMonth)) { + if (dataItem.year > maxYear || (dataItem.year === maxYear && dataItem.month1base > maxMonth)) { maxYear = dataItem.year; - maxMonth = dataItem.month; + maxMonth = dataItem.month1base; } } } diff --git a/src/models/transaction.ts b/src/models/transaction.ts index ad2b7182..ce4ef36e 100644 --- a/src/models/transaction.ts +++ b/src/models/transaction.ts @@ -1,5 +1,5 @@ import type { PartialRecord } from '@/core/base.ts'; -import type { YearMonth, StartEndTime } from '@/core/datetime.ts'; +import type { Year1BasedMonth, StartEndTime } from '@/core/datetime.ts'; import { type Coordinate, getNormalizedCoordinate } from '@/core/coordinate.ts'; import { TransactionType } from '@/core/transaction.ts'; @@ -508,7 +508,7 @@ export interface TransactionListByMaxTimeRequest { export interface TransactionListInMonthByPageRequest { readonly year: number; - readonly month: number; + readonly month: number; // 1-based (1 = January, 12 = December) readonly type: number; readonly categoryIds: string; readonly accountIds: string; @@ -672,13 +672,13 @@ export interface TransactionStatisticResponseItem { export interface TransactionStatisticTrendsResponseItem { readonly year: number; - readonly month: number; + readonly month: number; // 1-based (1 = January, 12 = December) readonly items: TransactionStatisticResponseItem[]; } -export interface YearMonthDataItem extends YearMonth, Record {} +export interface YearMonthDataItem extends Year1BasedMonth, Record {} -export interface YearMonthItems extends Record { +export interface YearMonthItems extends Record { readonly items: T[]; } @@ -718,9 +718,9 @@ export interface TransactionTrendsAnalysisDataItem extends TransactionStatisticD readonly items: TransactionTrendsAnalysisDataAmount[]; } -export interface TransactionTrendsAnalysisDataAmount { +export interface TransactionTrendsAnalysisDataAmount extends Year1BasedMonth { readonly year: number; - readonly month: number; + readonly month1base: number; readonly totalAmount: number; } diff --git a/src/stores/statistics.ts b/src/stores/statistics.ts index b59e4b9e..7de67897 100644 --- a/src/stores/statistics.ts +++ b/src/stores/statistics.ts @@ -76,7 +76,7 @@ interface TransactionStatisticResponseWithInfo { interface TransactionStatisticTrendsResponseItemWithInfo { readonly year: number; - readonly month: number; + readonly month: number; // 1-based (1 = January, 12 = December) readonly items: TransactionStatisticResponseItemWithInfo[]; } @@ -412,7 +412,7 @@ export const useStatisticsStore = defineStore('statistics', () => { combinedData.items.push({ year: trendItem.year, - month: trendItem.month, + month1base: trendItem.month, totalAmount: item.totalAmount }); diff --git a/src/stores/transaction.ts b/src/stores/transaction.ts index d67f2bdf..b46e7c50 100644 --- a/src/stores/transaction.ts +++ b/src/stores/transaction.ts @@ -96,7 +96,7 @@ export interface TransactionTotalAmount { export interface TransactionMonthList { readonly year: number; - readonly month: number; + readonly month: number; // 1-based (1 = January, 12 = December) readonly yearMonth: string; opened: boolean; readonly items: Transaction[];