[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[lmi-commits] [lmi] odd/string_db2 bba9667: Allow product lingo to vary
From: |
Greg Chicares |
Subject: |
[lmi-commits] [lmi] odd/string_db2 bba9667: Allow product lingo to vary by state, gender, and so on |
Date: |
Thu, 5 Nov 2020 12:04:26 -0500 (EST) |
branch: odd/string_db2
commit bba966795dea110a7d6dae079d1c001dd76b95a6
Author: Gregory W. Chicares <gchicares@sbcglobal.net>
Commit: Gregory W. Chicares <gchicares@sbcglobal.net>
Allow product lingo to vary by state, gender, and so on
This replaces commit ef5db171c2 on branch 'odd/string_db'. It is
presented as a single-commit new branch for convenient reference.
Writing strings to a '.lingo' file is the most important change.
See the commentary and simple example in 'ledger_invariant_init.cpp'
and the supporting code in 'dbdict.cpp'. Changed other files slightly
to make those changes build.
---
dbdict.cpp | 54 ++++++++++++++++++++++++
dbdict.hpp | 1 +
dbnames.hpp | 4 ++
dbnames.xpp | 2 +
ledger_invariant_init.cpp | 103 ++++++++++++++++++++++++++++++++++++++++++++++
5 files changed, 164 insertions(+)
diff --git a/dbdict.cpp b/dbdict.cpp
index e8b0a5b..3e724a1 100644
--- a/dbdict.cpp
+++ b/dbdict.cpp
@@ -419,6 +419,7 @@ void DBDictionary::ascribe_members()
ascribe("PartialMortTable" , &DBDictionary::PartialMortTable );
ascribe("UsePolicyFormAlt" , &DBDictionary::UsePolicyFormAlt );
ascribe("AllowGroupQuote" , &DBDictionary::AllowGroupQuote );
+ ascribe("L_PolicyForm" , &DBDictionary::L_PolicyForm );
ascribe("WeightClass" , &DBDictionary::WeightClass );
ascribe("WeightGender" , &DBDictionary::WeightGender );
ascribe("WeightSmoking" , &DBDictionary::WeightSmoking );
@@ -633,6 +634,52 @@ void DBDictionary::InitDB()
Add({DB_ExpSpecAmtLimit , dbl_inf});
}
+// Conceptually, this is in a proprietary TU.
+namespace
+{
+// Use nondefault initializers just to demonstrate that they work.
+enum e_proprietary_lingo
+ {e_policy_form = 13
+ ,e_policy_form_KS_KY = 0
+ };
+static std::unordered_map<e_proprietary_lingo,std::string> m
+ {{e_policy_form, "UL32768-NY"}
+ ,{e_policy_form_KS_KY, "UL32768-X"}
+ };
+
+void write_lingo_file(std::string const& filename)
+{
+ xml_lmi::xml_document document("lingo");
+ xml::element& root = document.root_node();
+ xml_serialize::to_xml(root, m);
+#if 0
+// Discarded alternative:
+ char const* name {"lingo_items"};
+ root.erase(name); // Unnecessary: new document.
+ xml_serialize::set_element(root, name, m);
+#endif // 0
+ document.save(filename);
+}
+} // Unnamed namespace.
+
+// Conceptually, this is in another public TU.
+//
+// A practical implementation might use something like
+// class lingo : public cache_file_reads<lingo>
+// to cache the map.
+std::unordered_map<int,std::string> const& load_lingo_file(std::string const&
filename)
+{
+ xml_lmi::dom_parser parser(filename);
+ xml::element const& root = parser.root_node("lingo");
+ static std::unordered_map<int,std::string> m2;
+ xml_serialize::from_xml(root, m2); // alternative to consider
+#if 0
+// Discarded alternative:
+ xml_serialize::get_element(root, "lingo_items", m2);
+#endif // 0
+ return m2;
+}
+
namespace
{
class sample : public DBDictionary {public: sample();};
@@ -927,6 +974,10 @@ sample::sample()
alt_form[mce_s_KY] = true;
Add({DB_UsePolicyFormAlt, premium_tax_dimensions, alt_form});
Add({DB_AllowGroupQuote , true});
+ std::vector<double> pol_form(e_max_dim_state, e_policy_form);
+ pol_form[mce_s_KS] = e_policy_form_KS_KY;
+ pol_form[mce_s_KY] = e_policy_form_KS_KY;
+ Add({DB_L_PolicyForm , premium_tax_dimensions, pol_form});
}
sample2finra::sample2finra()
@@ -989,6 +1040,9 @@ void DBDictionary::write_database_files()
sample2gpp ().WriteDB(AddDataDir("sample2gpp.database"));
sample2ipp ().WriteDB(AddDataDir("sample2ipp.database"));
sample2xyz ().WriteDB(AddDataDir("sample2xyz.database"));
+
+ // Temporary workaround.
+ write_lingo_file(AddDataDir("sample.lingo"));
}
/// Initialize the built-in database for the antediluvian branch.
diff --git a/dbdict.hpp b/dbdict.hpp
index d0a78ef..3d4b8fb 100644
--- a/dbdict.hpp
+++ b/dbdict.hpp
@@ -403,6 +403,7 @@ class LMI_SO DBDictionary
database_entity PartialMortTable ;
database_entity UsePolicyFormAlt ;
database_entity AllowGroupQuote ;
+ database_entity L_PolicyForm ;
database_entity WeightClass ;
database_entity WeightGender ;
database_entity WeightSmoking ;
diff --git a/dbnames.hpp b/dbnames.hpp
index 12bac7c..ab2694c 100644
--- a/dbnames.hpp
+++ b/dbnames.hpp
@@ -527,6 +527,10 @@ enum e_database_key
,DB_UsePolicyFormAlt
,DB_AllowGroupQuote
+ ,DB_Topic_Lingo
+
+ ,DB_L_PolicyForm
+
,DB_Topic_Weights
,DB_WeightClass
diff --git a/dbnames.xpp b/dbnames.xpp
index aa113ab..c5e5092 100644
--- a/dbnames.xpp
+++ b/dbnames.xpp
@@ -356,6 +356,8 @@
{DB_PartialMortTable,DB_Topic_Miscellanea,"PartialMortTable","Partial
mortality table (index in mortality table database)",}, \
{DB_UsePolicyFormAlt,DB_Topic_Miscellanea,"UsePolicyFormAlt","Use alternative
policy-form name: 0=no, 1=yes",}, \
{DB_AllowGroupQuote,DB_Topic_Miscellanea,"AllowGroupQuote","Allow group
premium quotes: 0=no, 1=yes",}, \
+{DB_Topic_Lingo,DB_FIRST,"Lingo","Substitutable text strings for report
generation",}, \
+{DB_L_PolicyForm,DB_Topic_Lingo,"L_PolicyForm","Policy form: string index",}, \
{DB_Topic_Weights,DB_FIRST,"Weights","Weights for profit analysis cells [not
yet implemented]",}, \
{DB_WeightClass,DB_Topic_Weights,"WeightClass","Weight by underwriting class
[not yet implemented]",}, \
{DB_WeightGender,DB_Topic_Weights,"WeightGender","Weight by gender [not yet
implemented]",}, \
diff --git a/ledger_invariant_init.cpp b/ledger_invariant_init.cpp
index 7639d82..1903034 100644
--- a/ledger_invariant_init.cpp
+++ b/ledger_invariant_init.cpp
@@ -27,6 +27,7 @@
#include "assert_lmi.hpp"
#include "basic_values.hpp"
#include "contains.hpp"
+#include "data_directory.hpp" // AddDataDir() [temporary kludge]
#include "database.hpp"
#include "dbnames.hpp"
#include "death_benefits.hpp"
@@ -34,6 +35,7 @@
#include "interest_rates.hpp"
#include "lmi.hpp" // is_antediluvian_fork()
#include "loads.hpp"
+#include "map_lookup.hpp"
#include "mc_enum_types_aux.hpp" // mc_str()
#include "miscellany.hpp" // each_equal()
#include "outlay.hpp"
@@ -44,6 +46,8 @@
#include <algorithm> // max(), max_element()
#include <stdexcept>
+std::unordered_map<int,std::string> const& load_lingo_file(std::string const&
filename);
+
/// Initialize with values determined by BasicValues construction.
///
/// This class's own ctor initializes all its data members, generally
@@ -324,7 +328,106 @@ void LedgerInvariant::Init(BasicValues const* b)
// Strings.
+// The next couple hundred "p.datum" lines will be replaced.
+//
+// Old way: store strings in class product_data. That works, but
+// strings are one-dimensional--not like the (up to) seven-dimensional
+// class database_entity used by class DBDictionary. A database_entity
+// is a 0- to 7-dimensional matrix of type 'double' (which might hold
+// actual floating point values, or integral values).
+//
+// Alternative not considered: devise a "string_database_entity" that
+// can hold a 0- to 7-dimensional string; then radically redesign
+// class product_data to hold that new type instead of plain strings.
+// Too much labor. Too much duplication.
+//
+// Crucial insight: Actuarial tables are zero-dimensional per se, but
+// lmi treats them as multidimensional by storing their table numbers
+// in objects of class database_entity, e.g.:
+//
+// // 1983 GAM; unisex=male because no unisex table was published.
+// double T83Gam[3] = {825, 826, 826,}; // f, m, u
+// Add({DB_PartialMortTable, e_number_of_axes, dims311, T83Gam});
+//
+// Something very similar can certainly be done for strings.
+//
+// New way: establish a compendium of company-specific strings, and
+// store indexes into that compendium in database_entity objects.
+// The only question is what form that compendium should take.
+// Answer: one XML '.lingo' file to be shared by a company's entire
+// portfolio of products. It is most convenient to associate each
+// string with an enumerator, to provide a descriptive name, but some
+// such names might be particular to the company, e.g.:
+//
+// static std::string const S_DefnCSV = // default for most products
+// "Cash surrender value is account value less any surrender charge.";
+//
+// static std::string const S_DefnCSV_turbo =
+// "Cash surrender value is account value less any surrender charge,"
+// " plus the Turbo Asset Boost provided by a special endorsement.";
+//
+// ...where each product-file entity already has a name, and its
+// string values are named with an "S_" prefix, and if necessary a
+// suffix indicating specialization--so a specialized footnote for a
+// particular product might naturally be named with a "_turbo" suffix,
+// for which a different company would have no use. Today's product
+// files might have
+// item("DefnAV") = S_DefnAV;
+// in the generic case, and
+// item("DefnAV") = S_DefnCSV_turbo;
+// for "turbo" products. In (proprietary) code that creates that
+// company's product files, it's convenient to provide a "turbo"
+// enumerator:
+// enum e_lingo
+// {e_defn_av
+// ,e_defn_av_turbo
+// };
+// static std::unordered_map<e_lingo,std::string> m
+// {{e_defn_av, S_DefnAV}
+// ,{e_defn_av_turbo, S_DefnCSV_turbo}
+// };
+// and use it to replace '.policy'-generation code like this:
+// item("DefnAV") = S_DefnCSV_turbo;
+// with '.database'-generation code like this:
+// Add({DB_L_DefnAV, e_defn_av_turbo});
+//
+// The proprietary "turbo" feature doesn't leak into lmi's public
+// code, because a proprietary 'e_lingo' is used only in proprietary
+// code that writes an XML file with integer rather than enumerative
+// keys, which are read into a map<int,std::string> in public code.
+//
+// What about strong typing? That's present on the write side, where
+// it can be used to ensure that no invalid "lingo" file is written.
+// Therefore, it's not very important on the read side, where it could
+// only verify the validity of a file already known to be valid. It's
+// not really needed on either side: actuarial-table numbers are naked
+// integers, yet they have worked just fine for decades.
+//
+// Here's a tiny example that refactors policy-form strings.
+// Today, 'sample' products have this in 'product_data.cpp':
+// item("PolicyForm") = glossed_string("UL32768-NY");
+// item("PolicyFormAlternative") = glossed_string("UL32768-X");
+// and this in 'dbdict.cpp':
+// // Use alternative policy form name in states beginning with "K".
+// std::vector<double> alt_form(e_max_dim_state);
+// alt_form[mce_s_KS] = true;
+// alt_form[mce_s_KY] = true;
+// Add({DB_UsePolicyFormAlt, premium_tax_dimensions, alt_form});
+// That's the old way:
+// - '.policy': contains every unique string (two in this case)
+// - '.database': indicates which of those strings to choose
+//
+// Here's the new way:
+
+ std::string const filename = AddDataDir("sample.lingo");
+ std::unordered_map<int,std::string> const& lingo_map =
load_lingo_file(filename);
+ auto policy_form = b->database().query<int>(DB_L_PolicyForm);
+ std::string PolicyForm_NEW = map_lookup(lingo_map, policy_form);
+// ...and that can replace the "p.datum" line immediately following,
+// as well as the assignment of local 'alt_form' above.
PolicyForm = p.datum(alt_form ? "PolicyFormAlternative" :
"PolicyForm");
+// Either way, the result is the same:
+LMI_ASSERT(PolicyForm_NEW == PolicyForm);
PolicyMktgName = p.datum("PolicyMktgName"
);
PolicyLegalName = p.datum("PolicyLegalName"
);
CsoEra = mc_str(b->database().query<mcenum_cso_era>(DB_CsoEra));