lmi-commits
[Top][All Lists]
Advanced

[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));



reply via email to

[Prev in Thread] Current Thread [Next in Thread]