From 4e4dcf1758cd12a6684f5fe30fb300613150bd18 Mon Sep 17 00:00:00 2001 From: Sherlock <119709043+agwekixj@users.noreply.github.com> Date: Sun, 2 Mar 2025 21:40:51 -0800 Subject: [PATCH] Use the fiscal start day of the month for relative quarter reports Only the quarter's start month in reports is adjusted when Absolute is selected in the Start Date on the Accounting Period preferences. This branch adjusts both the quarter's start day and month when Absolute is selected or when Relative and Today, Start of this month, or Start of previous month are selected. --- libgnucash/engine/gnc-option-date.cpp | 82 ++++++++++++++++----- libgnucash/engine/test/gtest-gnc-option.cpp | 61 +++++++++++++-- 2 files changed, 118 insertions(+), 25 deletions(-) diff --git a/libgnucash/engine/gnc-option-date.cpp b/libgnucash/engine/gnc-option-date.cpp index 7ab556ac34..a370681b3a 100644 --- a/libgnucash/engine/gnc-option-date.cpp +++ b/libgnucash/engine/gnc-option-date.cpp @@ -427,6 +427,26 @@ days_in_month(int month, int year) return gnc_date_get_last_mday(month, year + 1900); } +static int +get_last_day_of_month(struct tm& now) +{ + /* Ensure that the month is between 0 and 11*/ + auto year_delta = (now.tm_mon / 12) + (now.tm_mon < 0 ? -1 : 0); + return days_in_month(now.tm_mon - (12 * year_delta), now.tm_year + year_delta); +} + +static void +set_last_day_in_month(struct tm& now) +{ + now.tm_mday = get_last_day_of_month(now); +} + +static bool +is_last_day_in_month(struct tm& now) +{ + return now.tm_mday == get_last_day_of_month(now); +} + /* Normalize the modified struct tm computed in gnc_relative_date_to_time64 * before setting the time and perhaps beginning/end of the month. Using the * gnc_date API would involve multiple conversions to and from struct tm. @@ -464,20 +484,32 @@ normalize_reldate_tm(struct tm& now) } static void -reldate_set_day_and_time(struct tm& now, RelativeDateType type) +reldate_set_day_and_time( + struct tm& now, + RelativeDateType type, + bool is_offset_quarter, + struct tm& acct_per) { if (type == RelativeDateType::START) { - gnc_tm_set_day_start(&now); now.tm_mday = 1; + if (is_offset_quarter) + { + set_last_day_in_month(now); + if (!is_last_day_in_month(acct_per) && now.tm_mday > acct_per.tm_mday) + now.tm_mday = acct_per.tm_mday; + } + gnc_tm_set_day_start(&now); } else if (type == RelativeDateType::END) { - /* Ensure that the month is between 0 and 11*/ - auto year_delta = (now.tm_mon / 12) + (now.tm_mon < 0 ? -1 : 0); - auto month = now.tm_mon - (12 * year_delta); - auto year = now.tm_year + year_delta; - now.tm_mday = days_in_month(month, year); + set_last_day_in_month(now); + if (is_offset_quarter) + { + if (!is_last_day_in_month(acct_per) && now.tm_mday > acct_per.tm_mday) + now.tm_mday = acct_per.tm_mday; + --now.tm_mday; + } gnc_tm_set_day_end(&now); } // Do nothing for LAST and NEXT. @@ -497,15 +529,16 @@ gnc_relative_date_to_time64(RelativeDatePeriod period) if (period == RelativeDatePeriod::TODAY) return static_cast(now_t); auto now{static_cast(now_t)}; - struct tm acct_per{}; - if (gnc_prefs_get_bool (GNC_PREFS_GROUP_ACCT_SUMMARY, - GNC_PREF_START_CHOICE_ABS)) - acct_per = static_cast(GncDateTime(gnc_accounting_period_fiscal_start())); + struct tm acct_per = + static_cast(GncDateTime(gnc_accounting_period_fiscal_start())); + auto offset = reldate_offset(period); + bool is_offset_quarter = + offset == RelativeDateOffset::QUARTER && acct_per.tm_mday > 1; - switch(reldate_offset(period)) + switch(offset) { case RelativeDateOffset::NONE: -// Report on today so nothing to do + // Report on today so nothing to do break; case RelativeDateOffset::YEAR: if (reldate_is_prev(period)) @@ -517,14 +550,26 @@ gnc_relative_date_to_time64(RelativeDatePeriod period) else if (gnc_relative_date_is_ending(period)) now.tm_mon = 11; break; - case RelativeDateOffset::SIX: + case RelativeDateOffset::SIX: if (reldate_is_prev(period)) now.tm_mon -= 6; else if (reldate_is_next(period)) now.tm_mon += 6; break; case RelativeDateOffset::QUARTER: - now.tm_mon -= (12 + now.tm_mon - acct_per.tm_mon) % 3; + { + auto delta = (12 + now.tm_mon - acct_per.tm_mon) % 3; + if (is_offset_quarter) + { + if (delta == 0 && !is_last_day_in_month(now) && + (is_last_day_in_month(acct_per) || + now.tm_mday < acct_per.tm_mday)) + delta = 3; + if (gnc_relative_date_is_ending(period)) + --delta; + } + now.tm_mon -= delta; + } [[fallthrough]]; case RelativeDateOffset::THREE: if (reldate_is_prev(period)) @@ -534,7 +579,7 @@ gnc_relative_date_to_time64(RelativeDatePeriod period) if (gnc_relative_date_is_ending(period)) now.tm_mon += 2; break; - case RelativeDateOffset::MONTH: + case RelativeDateOffset::MONTH: if (reldate_is_prev(period)) --now.tm_mon; else if (reldate_is_next(period)) @@ -546,7 +591,10 @@ gnc_relative_date_to_time64(RelativeDatePeriod period) else if (reldate_is_next(period)) now.tm_mday += 7; } - reldate_set_day_and_time(now, checked_reldate(period).m_type); + reldate_set_day_and_time( now, + checked_reldate(period).m_type, + is_offset_quarter, + acct_per); normalize_reldate_tm(now); return static_cast(GncDateTime(now)); } diff --git a/libgnucash/engine/test/gtest-gnc-option.cpp b/libgnucash/engine/test/gtest-gnc-option.cpp index 7894f7c02a..cbda3a93fa 100644 --- a/libgnucash/engine/test/gtest-gnc-option.cpp +++ b/libgnucash/engine/test/gtest-gnc-option.cpp @@ -941,6 +941,51 @@ TEST(GncOptionDate, test_not_using_32bit_time_t_in_2038) EXPECT_FALSE(sizeof (time_t) == 4 && time(nullptr) <= 0) << "Time to upgrade 32bit time_t!"; } +// The relative current quarter start date is the date provided. +static void +set_current_quarter_start(GDate *date) +{ +} + +static GDateDay +get_last_day_of_month(GDate *date) +{ + return gnc_date_get_last_mday(g_date_get_month(date) - G_DATE_JANUARY, g_date_get_year(date)); +} + +static void +set_current_quarter_end(GDate *date) +{ + bool is_last_of_month = g_date_is_last_of_month(date); + GDateDay day = g_date_get_day(date); + GDateDay last_day_of_month; + + g_date_set_day(date, 1); + g_date_add_months(date, 3); + last_day_of_month = get_last_day_of_month(date); + g_date_set_day(date, is_last_of_month || day > last_day_of_month ? last_day_of_month : day); + g_date_subtract_days(date, 1); +} + +static void +set_previous_quarter_start(GDate *date) +{ + bool is_last_of_month = g_date_is_last_of_month(date); + GDateDay day = g_date_get_day(date); + GDateDay last_day_of_month; + + g_date_set_day(date, 1); + g_date_subtract_months(date, 3); + last_day_of_month = get_last_day_of_month(date); + g_date_set_day(date, is_last_of_month || day > last_day_of_month ? last_day_of_month : day); +} + +static void +set_previous_quarter_end(GDate *date) +{ + g_date_subtract_days(date, 1); +} + TEST(GncOptionDate, test_gnc_relative_date_to_time64) { GDate date; @@ -967,25 +1012,25 @@ TEST(GncOptionDate, test_gnc_relative_date_to_time64) EXPECT_EQ(time1, gnc_relative_date_to_time64(RelativeDatePeriod::END_PREV_MONTH)); g_date_set_time_t(&date, time(nullptr)); - gnc_gdate_set_quarter_start(&date); + set_current_quarter_start(&date); time1 = time64_from_gdate(&date, DayPart::start); EXPECT_EQ(time1, gnc_relative_date_to_time64(RelativeDatePeriod::START_CURRENT_QUARTER)); g_date_set_time_t(&date, time(nullptr)); - gnc_gdate_set_quarter_end(&date); + set_current_quarter_end(&date); time1 = time64_from_gdate(&date, DayPart::end); EXPECT_EQ(time1, gnc_relative_date_to_time64(RelativeDatePeriod::END_CURRENT_QUARTER)); g_date_set_time_t(&date, time(nullptr)); - gnc_gdate_set_prev_quarter_start(&date); + set_previous_quarter_start(&date); time1 = time64_from_gdate(&date, DayPart::start); EXPECT_EQ(time1, gnc_relative_date_to_time64(RelativeDatePeriod::START_PREV_QUARTER)); g_date_set_time_t(&date, time(nullptr)); - gnc_gdate_set_prev_quarter_end(&date); + set_previous_quarter_end(&date); time1 = time64_from_gdate(&date, DayPart::end); EXPECT_EQ(time1, gnc_relative_date_to_time64(RelativeDatePeriod::END_PREV_QUARTER)); @@ -1225,7 +1270,7 @@ TEST_F(GncDateOption, test_stream_in_quarter_start) { GDate date; g_date_set_time_t(&date, time(nullptr)); - gnc_gdate_set_quarter_start(&date); + set_current_quarter_start(&date); time64 time1{time64_from_gdate(&date, DayPart::start)}; std::istringstream iss{"relative . start-current-quarter"}; iss >> m_option; @@ -1236,7 +1281,7 @@ TEST_F(GncDateOption, test_stream_in_quarter_end) { GDate date; g_date_set_time_t(&date, time(nullptr)); - gnc_gdate_set_quarter_end(&date); + set_current_quarter_end(&date); time64 time1{time64_from_gdate(&date, DayPart::end)}; std::istringstream iss{"relative . end-current-quarter"}; iss >> m_option; @@ -1247,7 +1292,7 @@ TEST_F(GncDateOption, test_stream_in_prev_quarter_start) { GDate date; g_date_set_time_t(&date, time(nullptr)); - gnc_gdate_set_prev_quarter_start(&date); + set_previous_quarter_start(&date); time64 time1{time64_from_gdate(&date, DayPart::start)}; std::istringstream iss{"relative . start-prev-quarter"}; iss >> m_option; @@ -1258,7 +1303,7 @@ TEST_F(GncDateOption, test_stream_in_prev_quarter_end) { GDate date; g_date_set_time_t(&date, time(nullptr)); - gnc_gdate_set_prev_quarter_end(&date); + set_previous_quarter_end(&date); time64 time1{time64_from_gdate(&date, DayPart::end)}; std::istringstream iss{"relative . end-prev-quarter"}; iss >> m_option;