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;