From 3dc4bc237730adecd9b375c53c48a5e4958291d6 Mon Sep 17 00:00:00 2001 From: John Ralls Date: Tue, 29 Oct 2019 16:34:44 -0700 Subject: [PATCH] Implement GncOptionDateValue. --- libgnucash/app-utils/gnc-option.cpp | 96 +++++++++- libgnucash/app-utils/gnc-option.hpp | 87 +++++++-- libgnucash/app-utils/gnc-optiondb.cpp | 9 + libgnucash/app-utils/gnc-optiondb.hpp | 4 +- libgnucash/app-utils/gnc-optiondb.i | 8 + libgnucash/app-utils/test/CMakeLists.txt | 2 + .../app-utils/test/gtest-gnc-option.cpp | 166 ++++++++++++++++++ .../app-utils/test/gtest-gnc-optiondb.cpp | 8 + .../app-utils/test/test-gnc-optiondb.scm | 20 ++- 9 files changed, 381 insertions(+), 19 deletions(-) diff --git a/libgnucash/app-utils/gnc-option.cpp b/libgnucash/app-utils/gnc-option.cpp index 8543efa20c..278ca112c7 100644 --- a/libgnucash/app-utils/gnc-option.cpp +++ b/libgnucash/app-utils/gnc-option.cpp @@ -23,5 +23,99 @@ //#include "options.h" #include "gnc-option.hpp" -#include +#include +extern "C" +{ +#include "gnc-accounting-period.h" +} +static constexpr int days_in_month[12]{31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; + +static void +normalize_month(struct tm& now) +{ + if (now.tm_mon < 0) + { + now.tm_mon += 12; + --now.tm_year; + } + else if (now.tm_mon > 11) + { + now.tm_mon -= 12; + ++now.tm_year; + } +} + +static void +set_day_and_time(struct tm& now, bool starting) +{ + if (starting) + { + now.tm_hour = now.tm_min = now.tm_sec = 0; + now.tm_mday = 1; + } + else + { + now.tm_min = now.tm_sec = 59; + now.tm_hour = 23; + now.tm_mday = days_in_month[now.tm_mon]; + // Check for Februrary in a leap year + if (int year = now.tm_year + 1900; now.tm_mon == 1 && + year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)) + ++now.tm_mday; + } +}; + +time64 +GncOptionDateValue::get_value() const +{ + if (m_type == DateType::ABSOLUTE) + return m_date; + if (m_period == RelativeDatePeriod::TODAY) + return static_cast(GncDateTime()); + if (m_period == RelativeDatePeriod::ACCOUNTING_PERIOD) + return m_type == DateType::STARTING ? + gnc_accounting_period_fiscal_start() : + gnc_accounting_period_fiscal_end(); + + struct tm now{static_cast(GncDateTime())}; + struct tm period{static_cast(GncDateTime(gnc_accounting_period_fiscal_start()))}; + + if (m_period == RelativeDatePeriod::CAL_YEAR || + m_period == RelativeDatePeriod::PREV_YEAR) + { + if (m_period == RelativeDatePeriod::PREV_YEAR) + --now.tm_year; + now.tm_mon = m_type == DateType::STARTING ? 0 : 11; + } + else if (m_period == RelativeDatePeriod::PREV_QUARTER || + m_period == RelativeDatePeriod::CURRENT_QUARTER) + { + now.tm_mon = now.tm_mon - (now.tm_mon - period.tm_mon) % 3; + if (m_period == RelativeDatePeriod::PREV_QUARTER) + now.tm_mon -= 3; + if (m_type == DateType::ENDING) + now.tm_mon += 2; + } + else if (m_period == RelativeDatePeriod::PREV_MONTH) + --now.tm_mon; + normalize_month(now); + set_day_and_time(now, m_type == DateType::STARTING); + return static_cast(GncDateTime(now)); +} + +void +GncOptionDateValue::set_value(DateSetterValue value) +{ + auto [type, val] = value; + m_type = type; + if (type == DateType::ABSOLUTE) + { + m_period = RelativeDatePeriod::TODAY; + m_date = static_cast(val); + return; + } + + m_period = static_cast(val); + m_date = 0; +} diff --git a/libgnucash/app-utils/gnc-option.hpp b/libgnucash/app-utils/gnc-option.hpp index b80fb87187..522c2870be 100644 --- a/libgnucash/app-utils/gnc-option.hpp +++ b/libgnucash/app-utils/gnc-option.hpp @@ -31,6 +31,7 @@ extern "C" #include #include } +#include #include #include #include @@ -295,7 +296,6 @@ private: * * */ - using GncMultiChoiceOptionEntry = std::tuple; @@ -306,7 +306,7 @@ class GncOptionMultichoiceValue : { public: GncOptionMultichoiceValue(const char* section, const char* name, - const char* key, const char* doc_string, + const char* key, const char* doc_string, GncMultiChoiceOptionChoices&& choices, GncOptionUIType ui_type = GncOptionUIType::MULTICHOICE) : OptionClassifier{section, name, key, doc_string}, @@ -378,16 +378,75 @@ private: GncMultiChoiceOptionChoices m_choices; }; +/** Date options + * A legal date value is a pair of either and a RelativeDatePeriod, the absolute flag and a time64, or for legacy purposes the absolute flag and a timespec. + * The original design allowed custom RelativeDatePeriods, but that facility is unused so we'll go with compiled-in enums. + +gnc-date-option-show-time? -- option_data[1] +gnc-date-option-get-subtype -- option_data[0] +gnc-date-option-value-type m_value +gnc-date-option-absolute-time +gnc-date-option-relative-time + */ + +enum class DateType +{ + ABSOLUTE, + STARTING, + ENDING, +}; + +enum class RelativeDatePeriod : int64_t +{ + TODAY, + THIS_MONTH, + PREV_MONTH, + CURRENT_QUARTER, + PREV_QUARTER, + CAL_YEAR, + PREV_YEAR, + ACCOUNTING_PERIOD +}; + +using DateSetterValue = std::pair; +class GncOptionDateValue : public OptionClassifier, public OptionUIItem +{ +public: + GncOptionDateValue(const char* section, const char* name, + const char* key, const char* doc_string) : + OptionClassifier{section, name, key, doc_string}, + OptionUIItem(GncOptionUIType::DATE), + m_type{DateType::ABSOLUTE}, m_period{RelativeDatePeriod::TODAY}, + m_date{static_cast(GncDateTime())} {} + GncOptionDateValue(const GncOptionDateValue&) = default; + GncOptionDateValue(GncOptionDateValue&&) = default; + GncOptionDateValue& operator=(const GncOptionDateValue&) = default; + GncOptionDateValue& operator=(GncOptionDateValue&&) = default; + time64 get_value() const; + time64 get_default_value() const { return static_cast(GncDateTime()); } + void set_value(DateSetterValue); + void set_value(time64 time) { + m_type = DateType::ABSOLUTE; + m_period = RelativeDatePeriod::TODAY; + m_date = time; + } +private: + DateType m_type; + RelativeDatePeriod m_period; + time64 m_date; +}; + using GncOptionVariant = std::variant, - GncOptionValue, - GncOptionValue, - GncOptionValue, - GncOptionValue, - GncOptionValue>, - GncOptionMultichoiceValue, - GncOptionRangeValue, - GncOptionRangeValue, - GncOptionValidatedValue>; + GncOptionValue, + GncOptionValue, + GncOptionValue, + GncOptionValue, + GncOptionValue>, + GncOptionMultichoiceValue, + GncOptionRangeValue, + GncOptionRangeValue, + GncOptionValidatedValue, + GncOptionDateValue>; class GncOption { @@ -408,7 +467,7 @@ public: return std::visit([](const auto& option)->ValueType { if constexpr (std::is_same_v, std::decay_t>) return option.get_value(); - return ValueType {}; + return ValueType {}; }, m_option); } @@ -417,7 +476,7 @@ public: return std::visit([](const auto& option)->ValueType { if constexpr (std::is_same_v, std::decay_t>) return option.get_default_value(); - return ValueType {}; + return ValueType {}; }, m_option); } @@ -426,7 +485,7 @@ public: { std::visit([value](auto& option) { if constexpr (std::is_same_v, std::decay_t>) - option.set_value(value); + option.set_value(value); }, m_option); } const std::string& get_section() const diff --git a/libgnucash/app-utils/gnc-optiondb.cpp b/libgnucash/app-utils/gnc-optiondb.cpp index 46853a349d..814dccd9ee 100644 --- a/libgnucash/app-utils/gnc-optiondb.cpp +++ b/libgnucash/app-utils/gnc-optiondb.cpp @@ -453,3 +453,12 @@ gnc_register_currency_option(const GncOptionDBPtr& db, const char* section, }}; db->register_option(section, std::move(option)); } + +void +gnc_register_date_interval_option(const GncOptionDBPtr& db, const char* section, + const char* name, const char* key, + const char* doc_string) +{ + GncOption option{GncOptionDateValue(section, name, key, doc_string)}; + db->register_option(section, std::move(option)); +} diff --git a/libgnucash/app-utils/gnc-optiondb.hpp b/libgnucash/app-utils/gnc-optiondb.hpp index 2a9c597a2d..95319447fb 100644 --- a/libgnucash/app-utils/gnc-optiondb.hpp +++ b/libgnucash/app-utils/gnc-optiondb.hpp @@ -278,5 +278,7 @@ void gnc_register_dateformat_option(const GncOptionDBPtr& db, const char* key, const char* doc_string, std::string value); - +void gnc_register_date_interval_option(const GncOptionDBPtr& db, + const char* section, const char* name, + const char* key, const char* doc_string); #endif //GNC_OPTIONDB_HPP_ diff --git a/libgnucash/app-utils/gnc-optiondb.i b/libgnucash/app-utils/gnc-optiondb.i index b7314ac17f..33df360cfc 100644 --- a/libgnucash/app-utils/gnc-optiondb.i +++ b/libgnucash/app-utils/gnc-optiondb.i @@ -121,6 +121,13 @@ inline SCM %ignore GncOptionMultichoiceValue(GncOptionMultichoiceValue&&); %ignore GncOptionMultichoiceValue::operator=(const GncOptionMultichoiceValue&); %ignore GncOptionMultichoiceValue::operator=(GncOptionMultichoiceValue&&); +%ignore GncOptionDateValue(GncOptionDateValue&&); +%ignore GncOptionDateValue::operator=(const GncOptionDateValue&); +%ignore GncOptionDateValue::operator=(GncOptionDateValue&&); + +%typemap(typecheck, precedence=SWIG_TYPECHECK_INT64) time64 { + $1 = scm_is_signed_integer($input, INT64_MAX, INT64_MIN); +} %typemap(in) GncMultiChoiceOptionChoices&& (GncMultiChoiceOptionChoices choices) { @@ -168,6 +175,7 @@ wrap_unique_ptr(GncOptionDBPtr, GncOptionDB); %template(set_option_string) set_option; %template(set_option_int) set_option; + %template(set_option_time64) set_option; }; diff --git a/libgnucash/app-utils/test/CMakeLists.txt b/libgnucash/app-utils/test/CMakeLists.txt index d8fe3e53f8..dc41f34715 100644 --- a/libgnucash/app-utils/test/CMakeLists.txt +++ b/libgnucash/app-utils/test/CMakeLists.txt @@ -43,6 +43,7 @@ set(gtest_gnc_option_INCLUDES ${GUILE_INCLUDE_DIRS}) set(gtest_gnc_option_LIBS + gncmod-app-utils gncmod-engine ${GLIB2_LDFLAGS} ${GUILE_LDFLAGS} @@ -109,6 +110,7 @@ if (HAVE_SRFI64) set(swig_gnc_optiondb_LIBS gncmod-engine + gncmod-app-utils ${GLIB2_LDFLAGS} ${GUILE_LDFLAGS} ) diff --git a/libgnucash/app-utils/test/gtest-gnc-option.cpp b/libgnucash/app-utils/test/gtest-gnc-option.cpp index a209940983..59b04aaee9 100644 --- a/libgnucash/app-utils/test/gtest-gnc-option.cpp +++ b/libgnucash/app-utils/test/gtest-gnc-option.cpp @@ -23,6 +23,11 @@ #include #include +extern "C" +{ +#include +#include +} TEST(GncOption, test_string_ctor) { @@ -267,3 +272,164 @@ TEST_F(GncMultichoiceOption, test_permissible_value_stuff) EXPECT_EQ(std::numeric_limits::max(), m_option.permissible_value_index("xyzzy")); } + +class GncOptionDateOptionTest : public ::testing::Test +{ +protected: + GncOptionDateOptionTest() : + m_option{GncOptionDateValue{"foo", "bar", "a", "Phony Date Option"}} {} + + GncOptionDateValue m_option; +}; + +using GncDateOption = GncOptionDateOptionTest; + +static time64 +time64_from_gdate(const GDate* g_date, DayPart when) +{ + GncDate date{g_date_get_year(g_date), g_date_get_month(g_date), + g_date_get_day(g_date)}; + GncDateTime time{date, when}; + return static_cast(time); +} + +TEST_F(GncDateOption, test_set_and_get_absolute) +{ + time64 time1{static_cast(GncDateTime("2019-07-19 15:32:26 +05:00"))}; + DateSetterValue value1{DateType::ABSOLUTE, time1}; + m_option.set_value(value1); + EXPECT_EQ(time1, m_option.get_value()); +} + +TEST_F(GncDateOption, test_set_and_get_month_start) +{ + GDate month_start; + g_date_set_time_t(&month_start, time(nullptr)); + gnc_gdate_set_month_start(&month_start); + time64 time1{time64_from_gdate(&month_start, DayPart::start)}; + DateSetterValue value1{DateType::STARTING, static_cast(RelativeDatePeriod::THIS_MONTH)}; + m_option.set_value(value1); + EXPECT_EQ(time1, m_option.get_value()); +} + +TEST_F(GncDateOption, test_set_and_get_month_end) +{ + GDate month_end; + g_date_set_time_t(&month_end, time(nullptr)); + gnc_gdate_set_month_end(&month_end); + time64 time1{time64_from_gdate(&month_end, DayPart::end)}; + DateSetterValue value1{DateType::ENDING, static_cast(RelativeDatePeriod::THIS_MONTH)}; + m_option.set_value(value1); + EXPECT_EQ(time1, m_option.get_value()); +} + +TEST_F(GncDateOption, test_set_and_get_prev_month_start) +{ + GDate prev_month_start; + g_date_set_time_t(&prev_month_start, time(nullptr)); + gnc_gdate_set_prev_month_start(&prev_month_start); + time64 time1{time64_from_gdate(&prev_month_start, DayPart::start)}; + DateSetterValue value1{DateType::STARTING, static_cast(RelativeDatePeriod::PREV_MONTH)}; + m_option.set_value(value1); + EXPECT_EQ(time1, m_option.get_value()); +} + +TEST_F(GncDateOption, test_set_and_get_prev_month_end) +{ + GDate prev_month_end; + g_date_set_time_t(&prev_month_end, time(nullptr)); + gnc_gdate_set_prev_month_end(&prev_month_end); + time64 time1{time64_from_gdate(&prev_month_end, DayPart::end)}; + DateSetterValue value1{DateType::ENDING, static_cast(RelativeDatePeriod::PREV_MONTH)}; + m_option.set_value(value1); + EXPECT_EQ(time1, m_option.get_value()); +} + +TEST_F(GncDateOption, test_set_and_get_quarter_start) +{ + GDate quarter_start; + g_date_set_time_t(&quarter_start, time(nullptr)); + gnc_gdate_set_quarter_start(&quarter_start); + time64 time1{time64_from_gdate(&quarter_start, DayPart::start)}; + DateSetterValue value1{DateType::STARTING, static_cast(RelativeDatePeriod::CURRENT_QUARTER)}; + m_option.set_value(value1); + EXPECT_EQ(time1, m_option.get_value()); +} + +TEST_F(GncDateOption, test_set_and_get_quarter_end) +{ + GDate quarter_end; + g_date_set_time_t(&quarter_end, time(nullptr)); + gnc_gdate_set_quarter_end(&quarter_end); + time64 time1{time64_from_gdate(&quarter_end, DayPart::end)}; + DateSetterValue value1{DateType::ENDING, static_cast(RelativeDatePeriod::CURRENT_QUARTER)}; + m_option.set_value(value1); + EXPECT_EQ(time1, m_option.get_value()); +} + +TEST_F(GncDateOption, test_set_and_get_prev_quarter_start) +{ + GDate prev_quarter_start; + g_date_set_time_t(&prev_quarter_start, time(nullptr)); + gnc_gdate_set_prev_quarter_start(&prev_quarter_start); + time64 time1{time64_from_gdate(&prev_quarter_start, DayPart::start)}; + DateSetterValue value1{DateType::STARTING, static_cast(RelativeDatePeriod::PREV_QUARTER)}; + m_option.set_value(value1); + EXPECT_EQ(time1, m_option.get_value()); +} + +TEST_F(GncDateOption, test_set_and_get_prev_quarter_end) +{ + GDate prev_quarter_end; + g_date_set_time_t(&prev_quarter_end, time(nullptr)); + gnc_gdate_set_prev_quarter_end(&prev_quarter_end); + time64 time1{time64_from_gdate(&prev_quarter_end, DayPart::end)}; + DateSetterValue value1{DateType::ENDING, static_cast(RelativeDatePeriod::PREV_QUARTER)}; + m_option.set_value(value1); + EXPECT_EQ(time1, m_option.get_value()); +} + +TEST_F(GncDateOption, test_set_and_get_year_start) +{ + GDate year_start; + g_date_set_time_t(&year_start, time(nullptr)); + gnc_gdate_set_year_start(&year_start); + time64 time1{time64_from_gdate(&year_start, DayPart::start)}; + DateSetterValue value1{DateType::STARTING, static_cast(RelativeDatePeriod::CAL_YEAR)}; + m_option.set_value(value1); + EXPECT_EQ(time1, m_option.get_value()); +} + +TEST_F(GncDateOption, test_set_and_get_year_end) +{ + GDate year_end; + g_date_set_time_t(&year_end, time(nullptr)); + gnc_gdate_set_year_end(&year_end); + time64 time1{time64_from_gdate(&year_end, DayPart::end)}; + DateSetterValue value1{DateType::ENDING, static_cast(RelativeDatePeriod::CAL_YEAR)}; + m_option.set_value(value1); + EXPECT_EQ(time1, m_option.get_value()); +} + +TEST_F(GncDateOption, test_set_and_get_prev_year_start) +{ + GDate prev_year_start; + g_date_set_time_t(&prev_year_start, time(nullptr)); + gnc_gdate_set_prev_year_start(&prev_year_start); + time64 time1{time64_from_gdate(&prev_year_start, DayPart::start)}; + DateSetterValue value1{DateType::STARTING, static_cast(RelativeDatePeriod::PREV_YEAR)}; + m_option.set_value(value1); + EXPECT_EQ(time1, m_option.get_value()); +} + +TEST_F(GncDateOption, test_set_and_get_prev_year_end) +{ + GDate prev_year_end; + g_date_set_time_t(&prev_year_end, time(nullptr)); + gnc_gdate_set_prev_year_end(&prev_year_end); + time64 time1{time64_from_gdate(&prev_year_end, DayPart::end)}; + DateSetterValue value1{DateType::ENDING, static_cast(RelativeDatePeriod::PREV_YEAR)}; + m_option.set_value(value1); + EXPECT_EQ(time1, m_option.get_value()); +} + diff --git a/libgnucash/app-utils/test/gtest-gnc-optiondb.cpp b/libgnucash/app-utils/test/gtest-gnc-optiondb.cpp index 917e07fc5f..447fcda450 100644 --- a/libgnucash/app-utils/test/gtest-gnc-optiondb.cpp +++ b/libgnucash/app-utils/test/gtest-gnc-optiondb.cpp @@ -82,6 +82,14 @@ TEST_F(GncOptionDBTest, test_register_multichoice_option) EXPECT_STREQ("corge", m_db->lookup_string_option("foo", "bar").c_str()); } +TEST_F(GncOptionDBTest, test_register_date_interval_option) +{ + gnc_register_date_interval_option(m_db, "foo", "bar", "baz", "Phony Option"); + auto time{gnc_dmy2time64(11, 7, 2019)}; + ASSERT_TRUE(m_db->set_option("foo", "bar", time)); + EXPECT_EQ(time, m_db->find_option("foo", "bar")->get().get_value()); +} + class GncUIType { public: diff --git a/libgnucash/app-utils/test/test-gnc-optiondb.scm b/libgnucash/app-utils/test/test-gnc-optiondb.scm index d9c8450c8d..5916311b64 100644 --- a/libgnucash/app-utils/test/test-gnc-optiondb.scm +++ b/libgnucash/app-utils/test/test-gnc-optiondb.scm @@ -23,10 +23,12 @@ (use-modules (srfi srfi-64)) (use-modules (tests srfi64-extras)) - +(use-modules (gnucash gnc-module)) (eval-when (compile load eval expand) (load-extension "libswig-gnc-optiondb" "scm_init_sw_gnc_optiondb_module")) + +(gnc:module-load "gnucash/engine" 0) (use-modules (sw_gnc_optiondb)) (define (run-test) @@ -34,6 +36,7 @@ (test-begin "test-gnc-optiondb-scheme") (test-gnc-make-text-option) (test-gnc-make-multichoice-option) + (test-gnc-make-date-option) (test-end "test-gnc-optiondb-scheme")) (define (test-gnc-make-text-option) @@ -79,5 +82,16 @@ (test-equal "corge" (GncOptionDB-lookup-option (GncOptionDBPtr-get option-db) "foo" "bar"))) (test-end "test-gnc-test-multichoice-option")) - (GncOptionDBPtr-get option-db) "foo" "bar")) - (test-end "test-gnc-test-multichoice-option"))) + +(define (test-gnc-make-date-option) + (test-begin "test-gnc-test-date-option") + (let* ((option-db (gnc-option-db-new)) + (date-opt (gnc-register-date-interval-option option-db "foo" "bar" + "baz" "Phony Option")) + (a-time (gnc-dmy2time64 2019 07 11))) + (test-equal (current-time) (GncOptionDB-lookup-option + (GncOptionDBPtr-get option-db) "foo" "bar")) + (GncOptionDB-set-option-time64 (GncOptionDBPtr-get option-db) "foo" "bar" a-time) + (test-equal a-time (GncOptionDB-lookup-option + (GncOptionDBPtr-get option-db) "foo" "bar")) + (test-end "test-gnc-test-date-option")))