[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[lmi-commits] [lmi] gwc-no-xslfo a78b402 2/2: Import remaining new files
From: |
Greg Chicares |
Subject: |
[lmi-commits] [lmi] gwc-no-xslfo a78b402 2/2: Import remaining new files verbatim from vz-no-xslfo |
Date: |
Sat, 27 Jan 2018 02:09:54 -0500 (EST) |
branch: gwc-no-xslfo
commit a78b402a87d2954de9bc8bf98ecf0ccbe75cb294
Author: Gregory W. Chicares <address@hidden>
Commit: Gregory W. Chicares <address@hidden>
Import remaining new files verbatim from vz-no-xslfo
---
html.cpp | 128 ++
html.hpp | 327 +++++
interpolate_string.cpp | 296 +++++
interpolate_string.hpp | 67 +
interpolate_string_test.cpp | 233 ++++
ledger_evaluator.cpp | 864 +++++++++++++
ledger_evaluator.hpp | 59 +
ledger_pdf.cpp | 44 +
ledger_pdf.hpp | 35 +
ledger_pdf_generator.cpp | 46 +
ledger_pdf_generator.hpp | 63 +
ledger_pdf_generator_wx.cpp | 2940 +++++++++++++++++++++++++++++++++++++++++++
output_mode.hpp | 35 +
pdf_writer_wx.cpp | 249 ++++
pdf_writer_wx.hpp | 100 ++
15 files changed, 5486 insertions(+)
diff --git a/html.cpp b/html.cpp
new file mode 100644
index 0000000..95a68a9
--- /dev/null
+++ b/html.cpp
@@ -0,0 +1,128 @@
+// Utilities for representing and generating HTML.
+//
+// Copyright (C) 2017 Gregory W. Chicares.
+//
+// This program is free software; you can redistribute it and/or modify
+// it under the terms of the GNU General Public License version 2 as
+// published by the Free Software Foundation.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program; if not, write to the Free Software Foundation,
+// Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+//
+// http://savannah.nongnu.org/projects/lmi
+// email: <address@hidden>
+// snail: Chicares, 186 Belle Woods Drive, Glastonbury CT 06033, USA
+
+#include "pchfile.hpp"
+
+#include "html.hpp"
+
+#include <cstring>
+
+namespace html
+{
+
+namespace attr
+{
+
+extern attribute const align ("align");
+extern attribute const cellpadding ("cellpadding");
+extern attribute const cellspacing ("cellspacing");
+extern attribute const colspan ("colspan");
+extern attribute const nowrap ("nowrap");
+extern attribute const size ("size");
+extern attribute const valign ("valign");
+extern attribute const width ("width");
+
+} // namespace attr
+
+namespace tag
+{
+
+extern element const b ("b");
+extern void_element const br ("br");
+extern element const font ("font");
+extern element const i ("i");
+extern element const p ("p");
+extern element const table ("table");
+extern element const td ("td");
+extern element const tr ("tr");
+
+} // namespace tag
+
+std::string attribute::as_string() const
+{
+ std::string s(name_);
+ if(!value_.empty())
+ {
+ s += "=";
+ // TODO: Escape quotes.
+ s += value_;
+ }
+ return s;
+}
+
+namespace detail
+{
+
+std::string any_element::get_start() const
+{
+ std::string s("<");
+ // Extra +1 for the space before attributes, even if it's not needed.
+ s.reserve(1 + std::strlen(name_) + 1 + attributes_.length() + 1);
+ s += name_;
+ if(!attributes_.empty())
+ {
+ s += " ";
+ s += attributes_;
+ }
+ s += ">";
+ return s;
+}
+
+void any_element::update_attributes(attribute const& attr)
+{
+ if(attributes_.empty())
+ {
+ attributes_ = attr.as_string();
+ }
+ else
+ {
+ attributes_ += " ";
+ attributes_ += attr.as_string();
+ }
+}
+
+} // namespace detail
+
+void element::update_contents(std::string&& contents)
+{
+ if(contents_.empty())
+ {
+ contents_ = std::move(contents);
+ }
+ else
+ {
+ contents_ += contents;
+ }
+}
+
+element::operator text() const
+{
+ std::string s(get_start());
+ s.reserve(s.length() + contents_.length() + 2 + std::strlen(name_) + 1);
+ s += contents_;
+ s += "</";
+ s += name_;
+ s += ">";
+
+ return text::from_html(std::move(s));
+}
+
+} // namespace html
diff --git a/html.hpp b/html.hpp
new file mode 100644
index 0000000..552651b
--- /dev/null
+++ b/html.hpp
@@ -0,0 +1,327 @@
+// Utilities for representing and generating HTML.
+//
+// Copyright (C) 2017 Gregory W. Chicares.
+//
+// This program is free software; you can redistribute it and/or modify
+// it under the terms of the GNU General Public License version 2 as
+// published by the Free Software Foundation.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program; if not, write to the Free Software Foundation,
+// Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+//
+// http://savannah.nongnu.org/projects/lmi
+// email: <address@hidden>
+// snail: Chicares, 186 Belle Woods Drive, Glastonbury CT 06033, USA
+
+#ifndef html_hpp
+#define html_hpp
+
+#include "config.hpp"
+
+#include <string>
+#include <utility> // std::move
+
+/// Namespace for helpers used for HTML generation.
+///
+/// Main idea is to avoid generating HTML using raw strings, which is error
+/// prone and difficult to read and maintain. One source of errors is
+/// forgetting to escape special characters, such as "<" or "&", and html::text
+/// class helps with this by providing from() method doing it automatically.
+///
+/// Another one is forgetting to close a tag (or closing a wrong one) and while
+/// html::text is too low level to help with this, html::element can be used
+/// for structured HTML generation, which guarantees that the result is
+/// well-formed. By using predefined constants in html::tag and html::attr
+/// namespaces, typos in the element names can also be automatically avoided.
+namespace html
+{
+
+/// Represents a piece of text containing HTML.
+///
+/// This is a separate type for type safety, e.g. to avoid passing raw,
+/// unescaped, strings to a function expecting HTML (or, less catastrophically,
+/// but still wrongly, passing already escaped HTML to a function doing
+/// escaping internally).
+///
+/// As it still needs to be converted to a string sooner or later to be really
+/// used, it does provide a conversion -- but it can be used only once.
+class text
+{
+ public:
+ // This type has value semantics.
+ text() = default;
+ text(text const&) = default;
+ text(text&&) = default;
+ text& operator=(text const&) = default;
+ text& operator=(text&&) = default;
+
+ /// Escape special XML characters in the given string, ensuring that it
+ /// appears correctly inside HTML element contents. Notice that we don't
+ /// need to escape quotes here as we never use the result of this function
+ /// inside an HTML attribute, only inside HTML elements.
+ static text from(std::string const& s)
+ {
+ std::string z;
+ z.reserve(s.length());
+ for(auto const& c : s)
+ {
+ switch(c)
+ {
+ case '<': z += "<" ; break;
+ case '>': z += ">" ; break;
+ case '&': z += "&"; break;
+ default : z += c ;
+ }
+ }
+
+ return text{std::move(z)};
+ }
+
+ /// Use the given string with HTML inside it directly. No escaping is done
+ /// by this ctor.
+ static text from_html(std::string s)
+ {
+ return text{std::move(s)};
+ }
+
+ /// Just a symbolic name for a non breaking space HTML entiry.
+ static text nbsp()
+ {
+ return text::from_html(" ");
+ }
+
+ /// Append another text fragment to this one.
+ ///
+ /// This method allows chained invocation for appending more than one
+ /// fragment at once.
+ text& operator+=(text const& t)
+ {
+ m_html += t.m_html;
+
+ return *this;
+ }
+
+ std::string const& as_html() const&
+ {
+ return m_html;
+ }
+
+ std::string&& as_html() &&
+ {
+ return std::move(m_html);
+ }
+
+ private:
+ // This move ctor is private and does not perform any escaping.
+ explicit text(std::string&& html)
+ :m_html{html}
+ {
+ }
+
+ std::string m_html;
+};
+
+/// Represents a single attribute of an HTML element.
+class attribute
+{
+ public:
+ explicit attribute(char const* name)
+ :name_{name}
+ {
+ }
+
+ attribute operator()(std::string value) const
+ {
+ return attribute(name_, std::move(value));
+ }
+
+ std::string as_string() const;
+
+ private:
+ attribute(char const* name, std::string&& value)
+ :name_{name}
+ ,value_{std::move(value)}
+ {
+ }
+
+ char const* const name_;
+ std::string const value_;
+};
+
+namespace detail
+{
+
+class any_element
+{
+ public:
+ /// Ctor should only be used with literal strings as argument.
+ explicit any_element(char const* name)
+ :name_(name)
+ {
+ }
+
+ protected:
+ // Return the opening tag of the element, with attributes, if any.
+ std::string get_start() const;
+
+ // Add the given attribute to our attributes string.
+ void update_attributes(attribute const& attr);
+
+ char const* const name_;
+
+ private:
+ std::string attributes_;
+};
+
+} // namespace detail
+
+/// Represents a normal HTML element which can have content inside it.
+///
+/// This class uses the so called fluent API model in which calls to its
+/// different methods return the object itself and so can be chained together.
+/// For example (assuming an implicit "using namespace html"):
+///
+/// auto para_with_link =
+/// tag::p[attr::align("center")]
+/// (text("Link to "))
+/// (tag::a[attr::href("http://lmi.nongnu.org/")]
+/// (text::from("lmi project page"))
+/// )
+/// ;
+
+class element : private detail::any_element
+{
+ public:
+ /// Ctor should only be used with literal strings as argument.
+ explicit element(char const* name)
+ :detail::any_element(name)
+ {
+ }
+
+ element(element const&) = default;
+ element(element&&) = default;
+
+ /// Add an attribute.
+ element operator[](attribute const& attr) const&
+ {
+ element e{*this};
+ e.update_attributes(attr);
+ return e;
+ }
+
+ element&& operator[](attribute const& attr) &&
+ {
+ update_attributes(attr);
+ return std::move(*this);
+ }
+
+ /// Add inner contents.
+ element operator()(text contents) const&
+ {
+ element e{*this};
+ e.update_contents(std::move(contents).as_html());
+ return e;
+ }
+
+ element&& operator()(text contents) &&
+ {
+ update_contents(std::move(contents).as_html());
+ return std::move(*this);
+ }
+
+ /// Convert to HTML text with this element and its contents.
+ ///
+ /// This implicit conversion operator is not really dangerous as it is
+ /// normal to represent an HTML element as HTML text and it's very
+ /// convenient to have it as it allows to accept either another element or
+ /// text in our own operator() and also use operator+() defined below to
+ /// concatenate HTML elements without having to convert them to text
+ /// beforehand.
+ operator text() const;
+
+ private:
+ void update_contents(std::string&& contents);
+
+ std::string contents_;
+};
+
+/// Represents a void HTML element which can't have anything inside it.
+class void_element : private detail::any_element
+{
+ public:
+ explicit void_element(char const* name)
+ :detail::any_element(name)
+ {
+ }
+
+ void_element(void_element const&) = default;
+ void_element(void_element&&) = default;
+
+ void_element operator[](attribute const& attr) const&
+ {
+ void_element e{*this};
+ e.update_attributes(std::move(attr));
+ return e;
+ }
+
+ void_element&& operator[](attribute const& attr) &&
+ {
+ update_attributes(std::move(attr));
+ return std::move(*this);
+ }
+
+ operator text() const
+ {
+ return text::from_html(get_start());
+ }
+};
+
+/// Namespace for HTML attributes.
+
+namespace attr
+{
+
+extern attribute const align;
+extern attribute const cellpadding;
+extern attribute const cellspacing;
+extern attribute const colspan;
+extern attribute const nowrap;
+extern attribute const size;
+extern attribute const valign;
+extern attribute const width;
+
+} // namespace attr
+
+/// Namespace for HTML tags.
+
+namespace tag
+{
+
+extern element const b;
+extern void_element const br;
+extern element const font;
+extern element const i;
+extern element const p;
+extern element const table;
+extern element const td;
+extern element const tr;
+
+} // namespace tag
+
+inline
+text operator+(text t1, text const& t2)
+{
+ text t{std::move(t1)};
+ t += t2;
+ return t;
+}
+
+} // namespace html
+
+#endif // html_hpp
diff --git a/interpolate_string.cpp b/interpolate_string.cpp
new file mode 100644
index 0000000..28f6f4e
--- /dev/null
+++ b/interpolate_string.cpp
@@ -0,0 +1,296 @@
+// Interpolate string containing embedded variable references.
+//
+// Copyright (C) 2017 Gregory W. Chicares.
+//
+// This program is free software; you can redistribute it and/or modify
+// it under the terms of the GNU General Public License version 2 as
+// published by the Free Software Foundation.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program; if not, write to the Free Software Foundation,
+// Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+//
+// http://savannah.nongnu.org/projects/lmi
+// email: <address@hidden>
+// snail: Chicares, 186 Belle Woods Drive, Glastonbury CT 06033, USA
+
+#include "pchfile.hpp"
+
+#include "interpolate_string.hpp"
+
+#include "alert.hpp"
+
+#include <stack>
+#include <stdexcept>
+#include <vector>
+
+namespace
+{
+
+// Information about a single section that we're currently in.
+struct section_info
+{
+ section_info(std::string const& name, bool active)
+ :name_(name)
+ ,active_(active)
+ {
+ }
+
+ // Name of the section, i.e. the part after "#".
+ //
+ // TODO: In C++14 this could be replaced with string_view which would
+ // save on memory allocations without compromising safety, as we know
+ // that the input string doesn't change during this function execution.
+ std::string const name_;
+
+ // If true, output section contents, otherwise simply eat it.
+ bool const active_;
+
+ // Note: we could also store the position of the section start here to
+ // improve error reporting. Currently this is done as templates we use
+ // are small and errors shouldn't be difficult to find even without the
+ // exact position, but this could change in the future.
+};
+
+// The only context we need is the stack of sections entered so far.
+using context = std::stack<section_info, std::vector<section_info>>;
+
+// The real interpolation recursive function, called by the public one to do
+// all the work.
+void do_interpolate_string_in_context
+ (char const* s
+ ,lookup_function const& lookup
+ ,std::string& out
+ ,context& sections
+ ,std::string const& partial = std::string()
+ ,int recursion_level = 0
+ )
+{
+ // Guard against too deep recursion to avoid crashing on code using too
+ // many nested partials (either unintentionally, e.g. due to including a
+ // partial from itself, or maliciously).
+ //
+ // The maximum recursion level is chosen completely arbitrarily, the only
+ // criteria are that it shouldn't be too big to crash due to stack overflow
+ // before it is reached nor too small to break legitimate use cases.
+ if(recursion_level >= 100)
+ {
+ alarum()
+ << "Nesting level too deep while expanding the partial \""
+ << partial
+ << "\""
+ << std::flush
+ ;
+ }
+
+ // Check if the output is currently active or suppressed because we're
+ // inside an inactive section.
+ auto const is_active = [§ions]()
+ {
+ return sections.empty() || sections.top().active_;
+ };
+
+ for(char const* p = s; *p; ++p)
+ {
+ // As we know that the string is NUL-terminated, it is safe to check
+ // the next character.
+ if(p[0] == '{' && p[1] == '{')
+ {
+ std::string name;
+ auto const pos_start = p - s + 1;
+ for(p += 2;; ++p)
+ {
+ if(*p == '\0')
+ {
+ alarum()
+ << "Unmatched opening brace at position "
+ << pos_start
+ << std::flush
+ ;
+ }
+
+ if(p[0] == '}' && p[1] == '}')
+ {
+ switch(name.empty() ? '\0' : name[0])
+ {
+ case '#':
+ case '^':
+ {
+ auto const real_name = name.substr(1);
+ // If we're inside a disabled section, it doesn't
+ // matter whether this one is active or not.
+ bool active = is_active();
+ if(active)
+ {
+ auto const value = lookup
+ (real_name
+ ,interpolate_lookup_kind::section
+ );
+ if(value == "1")
+ {
+ active = true;
+ }
+ else if(value == "0")
+ {
+ active = false;
+ }
+ else
+ {
+ alarum()
+ << "Invalid value '"
+ << value
+ << "' of section '"
+ << real_name
+ << "' at position "
+ << pos_start
+ << ", only \"0\" or \"1\" allowed"
+ << std::flush
+ ;
+ }
+
+ if(name[0] == '^')
+ {
+ active = !active;
+ }
+ }
+
+ sections.emplace(real_name, active);
+ }
+ break;
+
+ case '/':
+ if(sections.empty())
+ {
+ alarum()
+ << "Unexpected end of section '"
+ << name.substr(1)
+ << "' at position "
+ << pos_start
+ << " without previous section start"
+ << std::flush
+ ;
+ }
+ if(name.compare(1, std::string::npos,
sections.top().name_) != 0)
+ {
+ alarum()
+ << "Unexpected end of section '"
+ << name.substr(1)
+ << "' at position "
+ << pos_start
+ << " while inside the section '"
+ << sections.top().name_
+ << "'"
+ << std::flush
+ ;
+ }
+ sections.pop();
+ break;
+
+ case '>':
+ if(is_active())
+ {
+ auto const& real_name = name.substr(1);
+
+ do_interpolate_string_in_context
+ (lookup
+ (real_name
+ ,interpolate_lookup_kind::partial
+ ).c_str()
+ ,lookup
+ ,out
+ ,sections
+ ,real_name
+ ,recursion_level + 1
+ );
+ }
+ break;
+
+ case '!':
+ // This is a comment, we just ignore it completely.
+ break;
+
+ default:
+ if(is_active())
+ {
+ // We don't check here if name is not empty, as
+ // there is no real reason to do it. Empty
+ // variable name may seem strange, but why not
+ // allow using "{{}}" to insert something into
+ // the interpolated string, after all?
+ out += lookup
+ (name
+ ,interpolate_lookup_kind::variable
+ );
+ }
+ }
+
+ // We consume two characters here ("}}"), not one, as in a
+ // usual loop iteration.
+ ++p;
+ break;
+ }
+
+ if(p[0] == '{' && p[1] == '{')
+ {
+ // We don't allow nested interpolations, so this can only
+ // be result of an error, e.g. a forgotten "}}" somewhere.
+ alarum()
+ << "Unexpected nested interpolation at position "
+ << pos_start
+ << " (outer interpolation starts at "
+ << (p - s - 1 - name.length())
+ << ")"
+ << std::flush
+ ;
+ }
+
+ // We don't impose any restrictions on the kind of characters
+ // that can occur in the variable names neither because there
+ // just doesn't seem to be anything to gain from it.
+ name += *p;
+ }
+ }
+ else if(is_active())
+ {
+ out += *p;
+ }
+ }
+}
+
+} // Unnamed namespace.
+
+std::string interpolate_string
+ (char const* s
+ ,lookup_function const& lookup
+ )
+{
+ std::string out;
+
+ // This is probably not going to be enough as replacements of the
+ // interpolated variables tend to be longer than the variables names
+ // themselves, but it's difficult to estimate the resulting string length
+ // any better than this.
+ out.reserve(strlen(s));
+
+ // The stack contains all the sections that we're currently in.
+ std::stack<section_info, std::vector<section_info>> sections;
+
+ do_interpolate_string_in_context(s, lookup, out, sections);
+
+ if(!sections.empty())
+ {
+ alarum()
+ << "Unclosed section '"
+ << sections.top().name_
+ << "'"
+ << std::flush
+ ;
+ }
+
+ return out;
+}
diff --git a/interpolate_string.hpp b/interpolate_string.hpp
new file mode 100644
index 0000000..e71d1ed
--- /dev/null
+++ b/interpolate_string.hpp
@@ -0,0 +1,67 @@
+// Interpolate string containing embedded variable references.
+//
+// Copyright (C) 2017 Gregory W. Chicares.
+//
+// This program is free software; you can redistribute it and/or modify
+// it under the terms of the GNU General Public License version 2 as
+// published by the Free Software Foundation.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program; if not, write to the Free Software Foundation,
+// Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+//
+// http://savannah.nongnu.org/projects/lmi
+// email: <address@hidden>
+// snail: Chicares, 186 Belle Woods Drive, Glastonbury CT 06033, USA
+
+#ifndef interpolate_string_hpp
+#define interpolate_string_hpp
+
+#include "config.hpp"
+
+#include <functional>
+#include <string>
+
+enum class interpolate_lookup_kind
+{
+ variable,
+ section,
+ partial
+};
+
+using lookup_function
+ = std::function<std::string (std::string const&, interpolate_lookup_kind)>;
+
+/// Interpolate string containing embedded variable references.
+///
+/// Return the input string after replacing all {{variable}} references in it
+/// with the value of the variable as returned by the provided function. The
+/// syntax is a (strict) subset of Mustache templates, the following features
+/// are supported:
+/// - Simple variable expansion for {{variable}}.
+/// - Conditional expansion using {{#variable}}...{{/variable}}.
+/// - Negated checks of the form {{^variable}}...{{/variable}}.
+/// - Partials support, i.e. {{>filename}}.
+///
+/// The following features are explicitly _not_ supported:
+/// - HTML escaping: this is done by a separate html::text class.
+/// - Separate types: 0/1 is false/true, anything else is an error.
+/// - Lists/section iteration (not needed yet).
+/// - Lambdas, comments, delimiter changes: omitted for simplicity.
+///
+/// To allow embedding literal "{{" fragment into the returned string, create a
+/// pseudo-variable expanding to these characters as its expansion, there is no
+/// built-in way to escape them.
+///
+/// Throw if the lookup function throws or if the string uses invalid syntax.
+std::string interpolate_string
+ (char const* s
+ ,lookup_function const& lookup
+ );
+
+#endif // interpolate_string_hpp
diff --git a/interpolate_string_test.cpp b/interpolate_string_test.cpp
new file mode 100644
index 0000000..2cf81be
--- /dev/null
+++ b/interpolate_string_test.cpp
@@ -0,0 +1,233 @@
+// Interpolate string containing embedded variable references.
+//
+// Copyright (C) 2017 Gregory W. Chicares.
+//
+// This program is free software; you can redistribute it and/or modify
+// it under the terms of the GNU General Public License version 2 as
+// published by the Free Software Foundation.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program; if not, write to the Free Software Foundation,
+// Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+//
+// http://savannah.nongnu.org/projects/lmi
+// email: <address@hidden>
+// snail: Chicares, 186 Belle Woods Drive, Glastonbury CT 06033, USA
+
+#include "pchfile.hpp"
+
+#include "interpolate_string.hpp"
+
+#include "test_tools.hpp"
+
+int test_main(int, char*[])
+{
+ auto const test_interpolate = [](char const* s)
+ {
+ return interpolate_string
+ (s
+ ,[](std::string const& k, interpolate_lookup_kind) { return k; }
+ );
+ };
+
+ // Check that basic interpolation works.
+ BOOST_TEST_EQUAL( test_interpolate(""), "" );
+ BOOST_TEST_EQUAL( test_interpolate("literal"), "literal" );
+ BOOST_TEST_EQUAL( test_interpolate("{{foo}}"), "foo" );
+ BOOST_TEST_EQUAL( test_interpolate("{{foo}}bar"), "foobar" );
+ BOOST_TEST_EQUAL( test_interpolate("foo{{}}bar"), "foobar" );
+ BOOST_TEST_EQUAL( test_interpolate("foo{{bar}}"), "foobar" );
+ BOOST_TEST_EQUAL( test_interpolate("{{foo}}{{bar}}"), "foobar" );
+
+ // Comments should be just ignored.
+ BOOST_TEST_EQUAL( test_interpolate("{{! ignore me}}"), "" );
+ BOOST_TEST_EQUAL( test_interpolate("{{! too}}{{x}}"), "x" );
+ BOOST_TEST_EQUAL( test_interpolate("{{x}}{{!also}}"), "x" );
+
+ // Sections.
+ auto const section_test = [](char const* str)
+ {
+ return interpolate_string
+ (str
+ ,[](std::string const& s, interpolate_lookup_kind) -> std::string
+ {
+ if(s == "var0") return "0";
+ if(s == "var1") return "1";
+ if(s == "var" ) return "" ;
+
+ throw std::runtime_error("no such variable '" + s + "'");
+ }
+ );
+ };
+
+ BOOST_TEST_EQUAL( section_test("x{{#var1}}y{{/var1}}z"), "xyz" );
+ BOOST_TEST_EQUAL( section_test("x{{#var0}}y{{/var0}}z"), "xz" );
+ BOOST_TEST_EQUAL( section_test("x{{^var0}}y{{/var0}}z"), "xyz" );
+ BOOST_TEST_EQUAL( section_test("x{{^var1}}y{{/var1}}z"), "xz" );
+
+ BOOST_TEST_EQUAL
+ (section_test("a{{#var1}}b{{#var1}}c{{/var1}}d{{/var1}}e")
+ ,"abcde"
+ );
+ BOOST_TEST_EQUAL
+ (section_test("a{{#var1}}b{{#var0}}c{{/var0}}d{{/var1}}e")
+ ,"abde"
+ );
+ BOOST_TEST_EQUAL
+ (section_test("a{{^var1}}b{{#var0}}c{{/var0}}d{{/var1}}e")
+ ,"ae"
+ );
+ BOOST_TEST_EQUAL
+ (section_test("a{{^var1}}b{{^var0}}c{{/var0}}d{{/var1}}e")
+ ,"ae"
+ );
+
+ // Partials.
+ auto const partial_test = [](char const* str)
+ {
+ return interpolate_string
+ (str
+ ,[](std::string const& s, interpolate_lookup_kind) -> std::string
+ {
+ if(s == "header") return "[header with {{var}}]";
+ if(s == "footer") return "[footer with {{var}}]";
+ if(s == "nested") return "[header with {{>footer}}]";
+ if(s == "recursive") return "{{>recursive}}";
+ if(s == "sec" ) return "1" ;
+ if(s == "var" ) return "variable" ;
+
+ throw std::runtime_error("no such variable '" + s + "'");
+ }
+ );
+ };
+
+ BOOST_TEST_EQUAL
+ (partial_test("{{>header}}")
+ ,"[header with variable]"
+ );
+
+ BOOST_TEST_EQUAL
+ (partial_test("{{>header}}{{var}} in body{{>footer}}")
+ ,"[header with variable]variable in body[footer with variable]"
+ );
+
+ BOOST_TEST_EQUAL
+ (partial_test("{{#sec}}{{>header}}{{/sec}}")
+ ,"[header with variable]"
+ );
+
+ BOOST_TEST_EQUAL
+ (partial_test("only{{^sec}}{{>header}}{{/sec}}{{>footer}}")
+ ,"only[footer with variable]"
+ );
+
+ BOOST_TEST_EQUAL
+ (partial_test("{{>nested}}")
+ ,"[header with [footer with variable]]"
+ );
+
+ BOOST_TEST_THROW
+ (partial_test("{{>recursive}}")
+ ,std::runtime_error
+ ,lmi_test::what_regex("Nesting level too deep")
+ );
+
+ BOOST_TEST_EQUAL
+ (partial_test("no {{^sec}}{{>recursive}}{{/sec}} problem")
+ ,"no problem"
+ );
+
+ // Some special cases.
+ BOOST_TEST_EQUAL
+ (interpolate_string
+ ("{{expanded}}"
+ ,[](std::string const& s, interpolate_lookup_kind) -> std::string
+ {
+ if(s == "expanded")
+ {
+ return "{{unexpanded}}";
+ }
+ throw std::runtime_error("no such variable '" + s + "'");
+ }
+ )
+ ,"{{unexpanded}}"
+ );
+
+ // Check that the kind of variable being expanded is correct.
+ BOOST_TEST_EQUAL
+ (interpolate_string
+ ("{{>test}}"
+ "{{#section1}}{{^section0}}{{variable}}{{/section0}}{{/section1}}"
+ ,[](std::string const& s, interpolate_lookup_kind kind) ->
std::string
+ {
+ switch(kind)
+ {
+ case interpolate_lookup_kind::variable:
+ return "value of " + s;
+
+ case interpolate_lookup_kind::section:
+ // Get rid of the "section" prefix.
+ return s.substr(7);
+
+ case interpolate_lookup_kind::partial:
+ return s + " partial included\n";
+ }
+
+ throw std::runtime_error("invalid lookup kind");
+ }
+ )
+ ,"test partial included\nvalue of variable"
+ );
+
+ // Should throw if the input syntax is invalid.
+ BOOST_TEST_THROW
+ (test_interpolate("{{x")
+ ,std::runtime_error
+ ,lmi_test::what_regex("Unmatched opening brace")
+ );
+ BOOST_TEST_THROW
+ (test_interpolate("{{x{{y}}}}")
+ ,std::runtime_error
+ ,lmi_test::what_regex("Unexpected nested interpolation")
+ );
+ BOOST_TEST_THROW
+ (section_test("{{#var1}}")
+ ,std::runtime_error
+ ,lmi_test::what_regex("Unclosed section 'var1'")
+ );
+ BOOST_TEST_THROW
+ (section_test("{{^var0}}")
+ ,std::runtime_error
+ ,lmi_test::what_regex("Unclosed section 'var0'")
+ );
+ BOOST_TEST_THROW
+ (section_test("{{/var1}}")
+ ,std::runtime_error
+ ,lmi_test::what_regex("Unexpected end of section")
+ );
+ BOOST_TEST_THROW
+ (section_test("{{#var1}}{{/var0}}")
+ ,std::runtime_error
+ ,lmi_test::what_regex("Unexpected end of section")
+ );
+
+ // Or because the lookup function throws.
+ BOOST_TEST_THROW
+ (interpolate_string
+ ("{{x}}"
+ ,[](std::string const& s, interpolate_lookup_kind) -> std::string
+ {
+ throw std::runtime_error("no such variable '" + s + "'");
+ }
+ )
+ ,std::runtime_error
+ ,"no such variable 'x'"
+ );
+
+ return EXIT_SUCCESS;
+}
diff --git a/ledger_evaluator.cpp b/ledger_evaluator.cpp
new file mode 100644
index 0000000..3e5ab24
--- /dev/null
+++ b/ledger_evaluator.cpp
@@ -0,0 +1,864 @@
+// Ledger evaluator returning values of all ledger fields.
+//
+// Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013,
2014, 2015, 2016, 2017 Gregory W. Chicares.
+//
+// This program is free software; you can redistribute it and/or modify
+// it under the terms of the GNU General Public License version 2 as
+// published by the Free Software Foundation.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program; if not, write to the Free Software Foundation,
+// Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+//
+// http://savannah.nongnu.org/projects/lmi
+// email: <address@hidden>
+// snail: Chicares, 186 Belle Woods Drive, Glastonbury CT 06033, USA
+
+#include "pchfile.hpp"
+
+#include "ledger_evaluator.hpp"
+
+#include "alert.hpp"
+#include "authenticity.hpp"
+#include "calendar_date.hpp"
+#include "configurable_settings.hpp"
+#include "contains.hpp"
+#include "global_settings.hpp"
+#include "handle_exceptions.hpp"
+#include "ledger.hpp"
+#include "ledger_invariant.hpp"
+#include "ledger_text_formats.hpp" // ledger_format()
+#include "ledger_variant.hpp"
+#include "map_lookup.hpp"
+#include "mc_enum_aux.hpp" // mc_e_vector_to_string_vector()
+#include "miscellany.hpp" // each_equal(), lmi_array_size()
+#include "oecumenic_enumerations.hpp"
+#include "value_cast.hpp"
+#include "version.hpp"
+
+#include <algorithm> // transform()
+#include <functional> // minus
+#include <unordered_map>
+#include <utility> // pair
+
+namespace
+{
+int const n = 7;
+
+char const* char_p_suffixes[n] =
+ {"_Current" // mce_run_gen_curr_sep_full
+ ,"_Guaranteed" // mce_run_gen_guar_sep_full
+ ,"_Midpoint" // mce_run_gen_mdpt_sep_full
+ ,"_CurrentZero" // mce_run_gen_curr_sep_zero
+ ,"_GuaranteedZero" // mce_run_gen_guar_sep_zero
+ ,"_CurrentHalf" // mce_run_gen_curr_sep_half
+ ,"_GuaranteedHalf" // mce_run_gen_guar_sep_half
+ };
+
+std::vector<std::string> const suffixes
+ (char_p_suffixes
+ ,char_p_suffixes + n
+ );
+
+typedef std::unordered_map<std::string, std::pair<int,oenum_format_style>>
format_map_t;
+typedef std::unordered_map<std::string, std::string> title_map_t;
+
+// For all numbers (so-called 'scalars' and 'vectors', but not
+// 'strings') grabbed from all ledgers, look for a format. If one
+// is found, use it to turn the number into a string. If not, and
+// the field is named in unavailable(), then it's ignored. Otherwise,
+// format_exists() displays a warning and ignores the field (because
+// throwing an exception would cause only the first warning to be
+// displayed).
+//
+// Rationale: Silently falling back on some default format can't be
+// right, because it masks defects that should be fixed: no default
+// can be universally appropriate.
+//
+// For names formed as
+// basename + '_' + suffix
+// only the basename is used as a map key. Lookups in the format map
+// are strict, as they must be, else one key like "A" would match
+// anything beginning with that letter.
+//
+// Some of the unavailable fields could easily be made available
+// someday; perhaps others should be eliminated from class Ledger.
+
+bool unavailable(std::string const& s)
+{
+ static std::string const a[] =
+ {"DateOfBirthJdn" // used by group quotes
+ ,"EffDateJdn" // used by group quotes
+ ,"ListBillDateJdn" // probably not needed
+ ,"InforceAsOfDateJdn" // probably not needed
+ ,"InitDacTaxRate" // used by PrintRosterTabDelimited(); not
cents
+ ,"InitPremTaxRate" // used by PrintRosterTabDelimited(); not
cents
+ ,"SubstdTable" // probably not needed
+ ,"InitMlyPolFee" // used by PrintRosterTabDelimited()
+ ,"InitTgtPremHiLoadRate" // used by PrintRosterTabDelimited(); not
cents
+ };
+ static std::vector<std::string> const v(a, a + lmi_array_size(a));
+ return contains(v, s);
+}
+
+bool format_exists
+ (std::string const& s
+ ,std::string const& suffix
+ ,format_map_t const& m
+ )
+{
+ if(contains(m, s))
+ {
+ return true;
+ }
+ else if(unavailable(s))
+ {
+ return false;
+ }
+ else
+ {
+ warning() << "No format found for " << s << suffix << LMI_FLUSH;
+ return false;
+ }
+}
+
+} // Unnamed namespace.
+
+std::string ledger_evaluator::operator()(std::string const& scalar) const
+{
+ return map_lookup(scalars_, scalar);
+}
+
+std::string ledger_evaluator::operator()
+ (std::string const& vector
+ ,std::size_t index
+ ) const
+{
+ return map_lookup(vectors_, vector).at(index);
+}
+
+ledger_evaluator Ledger::make_evaluator() const
+{
+ title_map_t title_map;
+
+// Can't seem to get a literal into the output.
+
+// Original: title_map["AttainedAge" ] = "
             
End of   Year Age";
+// No good: title_map["AttainedAge" ] = "
& & & & & & & & & & & & & 
End of & & Year Age";
+// No good: title_map["AttainedAge" ] = "
End of Year Age";
+// No good: title_map["AttainedAge" ] = "
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
End of &nbsp;&nbsp;Year Age";
+// No good: title_map["AttainedAge" ] = "<![CDATA[
             
End of   Year Age]]>";
+// No good: title_map["AttainedAge" ] = " ááááááááááááá
End of ááYear Age";
+// No good: title_map["AttainedAge" ] = "
             
End of   Year Age";
+
+// Here are the columns to be listed in the user interface
+// as well as their corresponding titles.
+
+ // Current and guaranteed variants are generally given for columns
+ // that vary by basis. Some offer only a current variant because
+ // they are defined only on a current basis--experience-rating
+ // columns, e.g.
+
+ title_map["AVGenAcct_CurrentZero" ] = "Curr
Charges\nAccount\nValue\nGen Acct";
+ title_map["AVGenAcct_GuaranteedZero" ] = "Guar
Charges\nAccount\nValue\nGen Acct";
+ title_map["AVRelOnDeath_Current" ] =
"Account\nValue\nReleased\non Death";
+ title_map["AVSepAcct_CurrentZero" ] = "Curr Charges\n0%
Account\nValue\nSep Acct";
+ title_map["AVSepAcct_GuaranteedZero" ] = "Guar Charges\n0%
Account\nValue\nSep Acct";
+ title_map["AcctVal_Current" ] = "Curr Account\nValue";
+ title_map["AcctVal_CurrentZero" ] = "Curr Charges\n0%
Account\nValue";
+ title_map["AcctVal_Guaranteed" ] = "Guar Account\nValue";
+ title_map["AcctVal_GuaranteedZero" ] = "Guar Charges\n0%
Account\nValue";
+ title_map["AddonCompOnAssets" ] = "Additional\nComp
on\nAssets";
+ title_map["AddonCompOnPremium" ] = "Additional\nComp
on\nPremium";
+ title_map["AddonMonthlyFee" ] = "Additional\nMonthly\nFee";
+ title_map["AnnGAIntRate_Current" ] = "Curr Ann\nGen Acct\nInt
Rate";
+ title_map["AnnGAIntRate_Guaranteed" ] = "Guar Ann\nGen Acct\nInt
Rate";
+ title_map["AnnHoneymoonValueRate_Current" ] = "Curr
Ann\nHoneymoon\nValue Rate";
+ title_map["AnnHoneymoonValueRate_Guaranteed"] = "Guar
Ann\nHoneymoon\nValue Rate";
+ title_map["AnnPostHoneymoonRate_Current" ] = "Curr
Post\nHoneymoon\nRate";
+ title_map["AnnPostHoneymoonRate_Guaranteed" ] = "Guar
Post\nHoneymoon\nRate";
+ title_map["AnnSAIntRate_Current" ] = "Curr Ann\nSep Acct\nInt
Rate";
+ title_map["AnnSAIntRate_Guaranteed" ] = "Guar Ann\nSep Acct\nInt
Rate";
+ title_map["AttainedAge" ] = "End of\nYear Age";
+ title_map["AvgDeathBft_Current" ] = "Curr Avg\nDeath\nBenefit";
+ title_map["AvgDeathBft_Guaranteed" ] = "Guar Avg\nDeath\nBenefit";
+ title_map["BaseDeathBft_Current" ] = "Curr
Base\nDeath\nBenefit";
+ title_map["BaseDeathBft_Guaranteed" ] = "Guar
Base\nDeath\nBenefit";
+ title_map["COICharge_Current" ] = "Curr COI\nCharge";
+ title_map["COICharge_Guaranteed" ] = "Guar COI\nCharge";
+ title_map["CSVNet_Current" ] = "Curr Net\nCash\nSurr
Value";
+ title_map["CSVNet_CurrentZero" ] = "Curr Charges\n0% Net
Cash\nSurr Value";
+ title_map["CSVNet_Guaranteed" ] = "Guar Net\nCash\nSurr
Value";
+ title_map["CSVNet_GuaranteedZero" ] = "Guar Charges\n0% Net
Cash\nSurr Value";
+ title_map["CV7702_Current" ] = "Curr 7702\nCash Value";
+ title_map["CV7702_Guaranteed" ] = "Guar 7702\nCash Value";
+ title_map["ClaimsPaid_Current" ] = "Curr\nClaims\nPaid";
+ title_map["ClaimsPaid_Guaranteed" ] = "Guar\nClaims\nPaid";
+ title_map["CorpTaxBracket" ] = "Corp Tax\nBracket";
+ title_map["CorridorFactor" ] = "Corridor\nFactor";
+ title_map["CurrMandE" ] =
"Mortality\nand\nExpense\nCharge";
+ title_map["DBOpt" ] = "Death\nBenefit\nOption";
+ title_map["DacTaxLoad_Current" ] = "Curr DAC\nTax\nLoad";
+ title_map["DacTaxLoad_Guaranteed" ] = "Guar DAC\nTax\nLoad";
+ title_map["DacTaxRsv_Current" ] = "Curr DAC\nTax\nReserve";
+ title_map["DacTaxRsv_Guaranteed" ] = "Guar DAC\nTax\nReserve";
+ title_map["DeathProceedsPaid_Current" ] = "Curr
Death\nProceeds\nPaid";
+ title_map["DeathProceedsPaid_Guaranteed" ] = "Guar
Death\nProceeds\nPaid";
+ title_map["EOYDeathBft_Current" ] = "Curr EOY\nDeath\nBenefit";
+ title_map["EOYDeathBft_Guaranteed" ] = "Guar EOY\nDeath\nBenefit";
+ title_map["EeGrossPmt" ] = "EE Gross\nPayment";
+ title_map["EeModalMinimumPremium" ] = "EE
Modal\nMinimum\nPremium";
+ title_map["EeMode" ] = "EE\nPayment\nMode";
+// TODO ?? This can't be a mode. I don't know how it differs from 'EeGrossPmt'
above.
+ title_map["EePmt" ] = "EE\nPayment\nMode";
+ title_map["ErGrossPmt" ] = "ER Gross\nPayment";
+ title_map["ErModalMinimumPremium" ] = "ER
Modal\nMinimum\nPremium";
+ title_map["ErMode" ] = "ER\nPayment\nMode";
+// TODO ?? This can't be a mode. I don't know how it differs from 'ErGrossPmt'
above.
+ title_map["ErPmt" ] = "ER\nPayment\nMode";
+ title_map["ExpenseCharges_Current" ] = "Curr\nExpense\nCharge";
+ title_map["ExpenseCharges_Guaranteed" ] = "Guar\nExpense\nCharge";
+ title_map["ExperienceReserve_Current" ] =
"Experience\nRating\nReserve";
+ title_map["GptForceout" ] = "Forceout";
+ title_map["GrossIntCredited_Current" ] = "Curr
Gross\nInt\nCredited";
+ title_map["GrossIntCredited_Guaranteed" ] = "Guar
Gross\nInt\nCredited";
+ title_map["GrossPmt" ] = "Premium\nOutlay";
+ title_map["HoneymoonValueSpread" ] = "Honeymoon\nValue\nSpread";
+ title_map["IndvTaxBracket" ] = "EE Tax\nBracket";
+ title_map["InforceLives" ] = "BOY\nLives\nInforce";
+ title_map["IrrCsv_Current" ] = "Curr IRR\non CSV";
+ title_map["IrrCsv_Guaranteed" ] = "Guar IRR\non CSV";
+ title_map["IrrDb_Current" ] = "Curr IRR\non DB";
+ title_map["IrrDb_Guaranteed" ] = "Guar IRR\non DB";
+ title_map["KFactor_Current" ] = "Experience\nRating\nK
Factor";
+ title_map["LoanIntAccrued_Current" ] = "Curr Loan\nInt\nAccrued";
+ title_map["LoanIntAccrued_Guaranteed" ] = "Guar Loan\nInt\nAccrued";
+ title_map["MlyGAIntRate_Current" ] = "Curr Monthly\nGen
Acct\nInt Rate";
+ title_map["MlyGAIntRate_Guaranteed" ] = "Guar Monthly\nGen
Acct\nInt Rate";
+ title_map["MlyHoneymoonValueRate_Current" ] = "Curr
Monthly\nHoneymoon\nValue Rate";
+ title_map["MlyHoneymoonValueRate_Guaranteed"] = "Guar
Monthly\nHoneymoon\nValue Rate";
+ title_map["MlyPostHoneymoonRate_Current" ] = "Curr
Monthly\nPost\nHoneymoon\nRate";
+ title_map["MlyPostHoneymoonRate_Guaranteed" ] = "Guar
Monthly\nPost\nHoneymoon\nRate";
+ title_map["MlySAIntRate_Current" ] = "Curr Monthly\nSep
Acct\nInt Rate";
+ title_map["MlySAIntRate_Guaranteed" ] = "Guar Monthly\nSep
Acct\nInt Rate";
+ title_map["ModalMinimumPremium" ] = "Modal\nMinimum\nPremium";
+ title_map["AnnualFlatExtra" ] = "Annual\nFlat\nExtra";
+// title_map["NaarForceout" ] = "Forced\nWithdrawal\ndue
to\nNAAR Limit";
+ title_map["NetCOICharge_Current" ] = "Experience\nRating\nNet
COI\nCharge";
+ title_map["NetClaims_Current" ] = "Curr Net\nClaims";
+ title_map["NetClaims_Guaranteed" ] = "Guar Net\nClaims";
+ title_map["NetIntCredited_Current" ] = "Curr Net\nInt\nCredited";
+ title_map["NetIntCredited_Guaranteed" ] = "Guar Net\nInt\nCredited";
+ title_map["NetPmt_Current" ] = "Curr Net\nPayment";
+ title_map["NetPmt_Guaranteed" ] = "Guar Net\nPayment";
+ title_map["NetWD" ] = "Withdrawal";
+ title_map["NewCashLoan" ] = "Annual Loan";
+ title_map["Outlay" ] = "Net Outlay";
+ title_map["PartMortTableMult" ] =
"Partial\nMortality\nMuliplier";
+ title_map["PolicyFee_Current" ] = "Curr\nPolicy\nFee";
+ title_map["PolicyFee_Guaranteed" ] = "Guar\nPolicy\nFee";
+ title_map["PolicyYear" ] = "Policy\nYear";
+ title_map["PrefLoanBalance_Current" ] = "Curr\nPreferred\nLoan
Bal";
+ title_map["PrefLoanBalance_Guaranteed" ] = "Guar\nPreferred\nLoan
Bal";
+ title_map["PremTaxLoad_Current" ] = "Curr\nPremium\nTax Load";
+ title_map["PremTaxLoad_Guaranteed" ] = "Guar\nPremium\nTax Load";
+// Excluded because it's unimplemented:
+// title_map["ProducerCompensation" ] = "Producer\nCompensation";
+ title_map["ProjectedCoiCharge_Current" ] =
"Experience\nRating\nProjected\nCOI Charge";
+ title_map["RefundableSalesLoad" ] = "Refundable\nSales\nLoad";
+ title_map["RiderCharges_Current" ] = "Curr Rider\nCharges";
+ title_map["Salary" ] = "Salary";
+ title_map["SepAcctCharges_Current" ] = "Curr Sep\nAcct\nCharges";
+ title_map["SepAcctCharges_Guaranteed" ] = "Guar Sep\nAcct\nCharges";
+ title_map["SpecAmt" ] = "Specified\nAmount";
+ title_map["SpecAmtLoad_Current" ] = "Curr Spec\nAmt Load";
+ title_map["SpecAmtLoad_Guaranteed" ] = "Guar Spec\nAmt Load";
+ title_map["SurrChg_Current" ] = "Curr Surr\nCharge";
+ title_map["SurrChg_Guaranteed" ] = "Guar Surr\nCharge";
+ title_map["TermPurchased_Current" ] = "Curr
Term\nAmt\nPurchased";
+ title_map["TermPurchased_Guaranteed" ] = "Guar
Term\nAmt\nPurchased";
+ title_map["TermSpecAmt" ] = "Term\nSpecified\nAmount";
+ title_map["TgtPrem" ] = "Target\nPremium";
+ title_map["TotalIMF" ] = "Total\nInvestment\nMgt
Fee";
+ title_map["TotalLoanBalance_Current" ] = "Curr
Total\nLoan\nBalance";
+ title_map["TotalLoanBalance_Guaranteed" ] = "Guar
Total\nLoan\nBalance";
+
+ // TODO ?? Titles ought to be read from an external file that
+ // permits flexible customization. Compliance might require that
+ // 'AcctVal_Current' be called "Cash Value" for one policy form,
+ // and "Account Value" for another, in order to match the terms
+ // used in the contract exactly. Therefore, these titles probably
+ // belong in the product database, which permits variation by
+ // product--though it does not accommodate strings as this is
+ // written in 2006-07. DATABASE !! So consider adding them there
+ // when the database is revamped.
+
+// Here's my top-level analysis of the formatting specification.
+//
+// Formats
+//
+// F0: zero decimals
+// F1: zero decimals, commas
+// F2: two decimals, commas
+// F3: scaled by 100, zero decimals, with '%' at end:
+// F4: scaled by 100, two decimals, with '%' at end:
+//
+// Presumably all use commas as thousands-separators, so that
+// an IRR of 12345.67% would be formatted as "12,345.67%".
+//
+// So the differences are:
+// 'precision' (number of decimal places)
+// percentage (scaled by 100, '%' at end) or not
+// and therefore F0 is equivalent to F1
+
+ std::pair<int,oenum_format_style> f1(0, oe_format_normal);
+ std::pair<int,oenum_format_style> f2(2, oe_format_normal);
+ std::pair<int,oenum_format_style> f3(0, oe_format_percentage);
+ std::pair<int,oenum_format_style> f4(2, oe_format_percentage);
+
+ format_map_t format_map;
+
+// > Special Formatting for Scalar Items
+// >
+// F4: scaled by 100, two decimals, with '%' at end:
+// > Format as percentage "0.00%"
+// >
+ format_map["GuarMaxMandE" ] = f4;
+ format_map["InitAnnGenAcctInt" ] = f4;
+ format_map["InitAnnLoanCredRate" ] = f4;
+ format_map["InitAnnLoanDueRate" ] = f4;
+ format_map["InitAnnSepAcctCurrGross0Rate" ] = f4;
+ format_map["InitAnnSepAcctCurrGrossHalfRate" ] = f4;
+ format_map["InitAnnSepAcctCurrNet0Rate" ] = f4;
+ format_map["InitAnnSepAcctCurrNetHalfRate" ] = f4;
+ format_map["InitAnnSepAcctGrossInt" ] = f4;
+ format_map["InitAnnSepAcctGuarGross0Rate" ] = f4;
+ format_map["InitAnnSepAcctGuarGrossHalfRate" ] = f4;
+ format_map["InitAnnSepAcctGuarNet0Rate" ] = f4;
+ format_map["InitAnnSepAcctGuarNetHalfRate" ] = f4;
+ format_map["InitAnnSepAcctNetInt" ] = f4;
+ format_map["PostHoneymoonSpread" ] = f4;
+ format_map["Preferred" ] = f4;
+ format_map["PremTaxRate" ] = f4;
+
+// F3: scaled by 100, zero decimals, with '%' at end:
+// > Format as percentage with no decimal places (##0%)
+ format_map["SalesLoadRefundRate0" ] = f3;
+ format_map["SalesLoadRefundRate1" ] = f3;
+ format_map["GenAcctAllocationPercent" ] = f3;
+ format_map["GenAcctAllocationComplementPercent"] = f3;
+
+// >
+// F2: two decimals, commas
+// > Format as a number with thousand separators and two decimal places
(#,###,###.00)
+// >
+ format_map["CurrentCoiMultiplier" ] = f2;
+ format_map["EeListBillPremium" ] = f2;
+ format_map["ErListBillPremium" ] = f2;
+ format_map["GuarPrem" ] = f2;
+ format_map["InforceTaxBasis" ] = f2;
+ format_map["InforceUnloanedAV" ] = f2;
+ format_map["InitGLP" ] = f2;
+ format_map["InitGSP" ] = f2;
+ format_map["InitPrem" ] = f2;
+ format_map["InitSevenPayPrem" ] = f2;
+ format_map["InitTgtPrem" ] = f2;
+ format_map["InitMinPrem" ] = f2;
+ format_map["ListBillPremium" ] = f2;
+ format_map["ModalMinimumDumpin" ] = f2;
+// >
+// F1: zero decimals, commas
+// > Format as a number with thousand separators and no decimal places
(#,###,###)
+// >
+ format_map["Age" ] = f1;
+ format_map["AllowDbo3" ] = f1;
+ format_map["AvgFund" ] = f1;
+ format_map["ChildRiderAmount" ] = f1;
+ format_map["CustomFund" ] = f1;
+ format_map["Dumpin" ] = f1;
+ format_map["EndtAge" ] = f1;
+ format_map["External1035Amount" ] = f1;
+ format_map["GenAcctAllocation" ] = f1;
+ format_map["GenderBlended" ] = f1;
+ format_map["GenderDistinct" ] = f1;
+ format_map["Has1035ExchCharge" ] = f1;
+ format_map["HasADD" ] = f1;
+ format_map["HasChildRider" ] = f1;
+ format_map["HasHoneymoon" ] = f1;
+ format_map["HasSalesLoadRefund" ] = f1;
+ format_map["HasSpouseRider" ] = f1;
+ format_map["HasSupplSpecAmt" ] = f1;
+ format_map["HasTerm" ] = f1;
+ format_map["HasWP" ] = f1;
+ format_map["InforceIsMec" ] = f1;
+ format_map["InforceMonth" ] = f1;
+ format_map["InforceYear" ] = f1;
+ format_map["InitBaseSpecAmt" ] = f1;
+ format_map["InitTermSpecAmt" ] = f1;
+ format_map["InitTotalSA" ] = f1;
+ format_map["Internal1035Amount" ] = f1;
+ format_map["IsInforce" ] = f1;
+ format_map["IsMec" ] = f1;
+ format_map["LapseMonth" ] = f1;
+ format_map["LapseYear" ] = f1;
+ format_map["MaxDuration" ] = f1;
+ format_map["MecMonth" ] = f1;
+ format_map["MecYear" ] = f1;
+ format_map["NoLapse" ] = f1;
+ format_map["NoLapseAlwaysActive" ] = f1;
+ format_map["NoLapseMinAge" ] = f1;
+ format_map["NoLapseMinDur" ] = f1;
+ format_map["RetAge" ] = f1;
+ format_map["SmokerBlended" ] = f1;
+ format_map["SmokerDistinct" ] = f1;
+ format_map["SplitFundAllocation" ] = f1;
+ format_map["SplitMinPrem" ] = f1;
+ format_map["SpouseIssueAge" ] = f1;
+ format_map["SupplementalReport" ] = f1;
+ format_map["UseExperienceRating" ] = f1;
+ format_map["GroupIndivSelection" ] = f1;
+ format_map["UsePartialMort" ] = f1;
+
+// > Vector Formatting
+// >
+// > Here are the vectors enumerated
+// >
+// F3: scaled by 100, zero decimals, with '%' at end:
+// > Format as percentage with no decimal places (##0%)
+// >
+ format_map["CorridorFactor" ] = f3;
+ format_map["FundAllocations" ] = f3;
+ format_map["MaleProportion" ] = f3;
+ format_map["NonsmokerProportion" ] = f3;
+ format_map["PartMortTableMult" ] = f3;
+
+// >
+// F4: scaled by 100, two decimals, with '%' at end:
+// > Format as percentage with two decimal places (##0.00%)
+// >
+ format_map["AnnGAIntRate" ] = f4;
+ format_map["AnnHoneymoonValueRate" ] = f4;
+ format_map["AnnPostHoneymoonRate" ] = f4;
+ format_map["AnnSAIntRate" ] = f4;
+ format_map["CashFlowIRR" ] = f4;
+ format_map["CorpTaxBracket" ] = f4;
+ format_map["CurrMandE" ] = f4;
+ format_map["HoneymoonValueSpread" ] = f4;
+ format_map["IndvTaxBracket" ] = f4;
+ format_map["InforceHMVector" ] = f4;
+
+ format_map["IrrCsv_Current" ] = f4;
+ format_map["IrrCsv_CurrentZero" ] = f4;
+ format_map["IrrCsv_Guaranteed" ] = f4;
+ format_map["IrrCsv_GuaranteedZero" ] = f4;
+ format_map["IrrDb_Current" ] = f4;
+ format_map["IrrDb_CurrentZero" ] = f4;
+ format_map["IrrDb_Guaranteed" ] = f4;
+ format_map["IrrDb_GuaranteedZero" ] = f4;
+
+ format_map["MlyGAIntRate" ] = f4;
+ format_map["MlyHoneymoonValueRate" ] = f4;
+ format_map["MlyPostHoneymoonRate" ] = f4;
+ format_map["MlySAIntRate" ] = f4;
+ format_map["TotalIMF" ] = f4;
+// >
+// F0: zero decimals
+// > Format as a number no thousand separator or decimal point (##0%)
+// >
+ format_map["AttainedAge" ] = f1;
+ format_map["Duration" ] = f1;
+ format_map["LapseYears" ] = f1;
+ format_map["PolicyYear" ] = f1;
+// >
+// F2: two decimals, commas
+// > Format as a number with thousand separators and two decimal places
(#,###,###.00)
+// >
+ format_map["AddonMonthlyFee" ] = f2;
+// TODO ?? The precision of 'InforceLives' and 'KFactor' is inadequate.
+// Is every other format OK?
+ format_map["InforceLives" ] = f2;
+ format_map["KFactor" ] = f2;
+ format_map["AnnualFlatExtra" ] = f2;
+// >
+// F1: zero decimals, commas
+// > Format as a number with thousand separators and no decimal places
(#,###,##0)
+// >
+ format_map["AcctVal" ] = f1;
+ format_map["AccumulatedPremium" ] = f1;
+ format_map["AddonCompOnAssets" ] = f1;
+ format_map["AddonCompOnPremium" ] = f1;
+ format_map["AvgDeathBft" ] = f1;
+ format_map["AVGenAcct" ] = f1;
+ format_map["AVRelOnDeath" ] = f1;
+ format_map["AVSepAcct" ] = f1;
+ format_map["BaseDeathBft" ] = f1;
+ format_map["BOYAssets" ] = f1;
+ format_map["ClaimsPaid" ] = f1;
+ format_map["COICharge" ] = f1;
+ format_map["Composite" ] = f1;
+ format_map["CSVNet" ] = f1;
+ format_map["CV7702" ] = f1;
+ format_map["DacTaxLoad" ] = f1;
+ format_map["DacTaxRsv" ] = f1;
+ format_map["DeathProceedsPaid" ] = f1;
+ format_map["EeGrossPmt" ] = f1;
+ format_map["EeModalMinimumPremium" ] = f1;
+// format_map["EeMode" ] = f1; // Not numeric.
+ format_map["EePmt" ] = f1;
+ format_map["EOYDeathBft" ] = f1;
+ format_map["ErGrossPmt" ] = f1;
+ format_map["ErModalMinimumPremium" ] = f1;
+// format_map["ErMode" ] = f1; // Not numeric.
+ format_map["ErPmt" ] = f1;
+ format_map["ExpenseCharges" ] = f1;
+ format_map["ExperienceReserve" ] = f1;
+ format_map["FundNumbers" ] = f1;
+ format_map["GptForceout" ] = f1;
+ format_map["GrossIntCredited" ] = f1;
+ format_map["GrossPmt" ] = f1;
+ format_map["Loads" ] = f1;
+ format_map["LoanInt" ] = f1;
+ format_map["LoanIntAccrued" ] = f1;
+ format_map["ModalMinimumPremium" ] = f1;
+ format_map["NaarForceout" ] = f1;
+ format_map["NetClaims" ] = f1;
+ format_map["NetCOICharge" ] = f1;
+ format_map["NetIntCredited" ] = f1;
+ format_map["NetPmt" ] = f1;
+ format_map["NetWD" ] = f1;
+ format_map["NewCashLoan" ] = f1;
+ format_map["Outlay" ] = f1;
+ format_map["PolicyFee" ] = f1;
+ format_map["PrefLoanBalance" ] = f1;
+ format_map["PremTaxLoad" ] = f1;
+ format_map["ProducerCompensation" ] = f1;
+ format_map["ProjectedCoiCharge" ] = f1;
+ format_map["RefundableSalesLoad" ] = f1;
+ format_map["RiderCharges" ] = f1;
+ format_map["Salary" ] = f1;
+ format_map["SepAcctCharges" ] = f1;
+ format_map["SpecAmt" ] = f1;
+ format_map["SpecAmtLoad" ] = f1;
+ format_map["SpouseRiderAmount" ] = f1;
+ format_map["SurrChg" ] = f1;
+ format_map["TermPurchased" ] = f1;
+ format_map["TermSpecAmt" ] = f1;
+ format_map["TgtPrem" ] = f1;
+ format_map["TotalLoanBalance" ] = f1;
+
+ // This is a little tricky. We have some stuff that
+ // isn't in the maps inside the ledger classes. We're going to
+ // stuff it into a copy of the invariant-ledger class's data.
+ // To avoid copying, we'll use pointers to the data. Most of
+ // this stuff is invariant anyway, so that's a reasonable
+ // place to put it.
+ //
+ // First we make a copy of the invariant ledger:
+
+ double_vector_map vectors = ledger_invariant_->AllVectors;
+ scalar_map scalars = ledger_invariant_->AllScalars;
+ string_map strings = ledger_invariant_->Strings;
+
+ // Now we add the stuff that wasn't in the invariant
+ // ledger's class's maps (indexable by name). Because we're
+ // working with maps of pointers, we need pointers here.
+ //
+ // The IRRs are the worst of all.
+
+ if(!ledger_invariant_->IsInforce)
+ {
+ ledger_invariant_->CalculateIrrs(*this);
+ }
+ vectors["IrrCsv_GuaranteedZero" ] = &ledger_invariant_->IrrCsvGuar0 ;
+ vectors["IrrDb_GuaranteedZero" ] = &ledger_invariant_->IrrDbGuar0 ;
+ vectors["IrrCsv_CurrentZero" ] = &ledger_invariant_->IrrCsvCurr0 ;
+ vectors["IrrDb_CurrentZero" ] = &ledger_invariant_->IrrDbCurr0 ;
+ vectors["IrrCsv_Guaranteed" ] = &ledger_invariant_->IrrCsvGuarInput;
+ vectors["IrrDb_Guaranteed" ] = &ledger_invariant_->IrrDbGuarInput ;
+ vectors["IrrCsv_Current" ] = &ledger_invariant_->IrrCsvCurrInput;
+ vectors["IrrDb_Current" ] = &ledger_invariant_->IrrDbCurrInput ;
+
+// GetMaxLength() is max *composite* length.
+// int max_length = GetMaxLength();
+ double MaxDuration = ledger_invariant_->EndtAge - ledger_invariant_->Age;
+ scalars["MaxDuration"] = &MaxDuration;
+ int max_duration = static_cast<int>(MaxDuration);
+
+ std::vector<double> PolicyYear;
+ std::vector<double> AttainedAge;
+
+ PolicyYear .resize(max_duration);
+ AttainedAge.resize(max_duration);
+
+ int issue_age = static_cast<int>(ledger_invariant_->Age);
+ for(int j = 0; j < max_duration; ++j)
+ {
+ PolicyYear[j] = 1 + j;
+ AttainedAge[j] = 1 + j + issue_age;
+ }
+
+// TODO ?? An attained-age column is meaningless in a composite. So
+// are several others--notably those affected by partial mortaility.
+ vectors["AttainedAge"] = &AttainedAge;
+ vectors["PolicyYear" ] = &PolicyYear ;
+
+ std::vector<double> InitAnnLoanDueRate(max_duration);
+ std::fill
+ (InitAnnLoanDueRate.begin()
+ ,InitAnnLoanDueRate.end()
+ ,ledger_invariant_->GetInitAnnLoanDueRate()
+ );
+ vectors["InitAnnLoanDueRate"] = &InitAnnLoanDueRate;
+
+ vectors["InforceLives"] = &ledger_invariant_->InforceLives;
+
+ vectors["FundNumbers" ] = &ledger_invariant_->FundNumbers ;
+ vectors["FundAllocations"] = &ledger_invariant_->FundAllocations;
+
+ // The Ledger object should contain a basic minimal set of columns
+ // from which others may be derived. It must be kept small because
+ // its size imposes a practical limit on the number of lives that
+ // can be run as part of a single census.
+ //
+ // TODO ?? A really good design would give users the power to
+ // define and store their own derived-column definitions. For now,
+ // however, code changes are required, and this is as appropriate
+ // a place as any to make them.
+
+ LedgerInvariant const& Invar = GetLedgerInvariant();
+ LedgerVariant const& Curr_ = GetCurrFull();
+ LedgerVariant const& Guar_ = GetGuarFull();
+
+ std::vector<double> PremiumLoads(max_duration);
+ std::vector<double> AdminCharges(max_duration);
+ for(int j = 0; j < max_duration; ++j)
+ {
+ PremiumLoads[j] = Invar.GrossPmt[j] - Curr_.NetPmt[j];
+ AdminCharges[j] = Curr_.SpecAmtLoad[j] + Curr_.PolicyFee[j];
+ }
+
+ vectors ["PremiumLoads"] = &PremiumLoads;
+ format_map["PremiumLoads"] = f1;
+ vectors ["AdminCharges"] = &AdminCharges;
+ format_map["AdminCharges"] = f1;
+
+ // ET !! Easier to write as
+ // std::vector<double> NetDeathBenefit =
+ // Curr_.EOYDeathBft - Curr_.TotalLoanBalance;
+ std::vector<double> NetDeathBenefit(Curr_.EOYDeathBft);
+ std::transform
+ (NetDeathBenefit.begin()
+ ,NetDeathBenefit.end()
+ ,Curr_.TotalLoanBalance.begin()
+ ,NetDeathBenefit.begin()
+ ,std::minus<double>()
+ );
+ vectors ["NetDeathBenefit"] = &NetDeathBenefit;
+ title_map ["NetDeathBenefit"] = "Net\nDeath\nBenefit";
+ format_map["NetDeathBenefit"] = f1;
+
+ std::vector<double> SupplDeathBft_Current (Curr_.TermPurchased);
+ std::vector<double> SupplDeathBft_Guaranteed(Guar_.TermPurchased);
+ vectors ["SupplDeathBft_Current" ] = &SupplDeathBft_Current;
+ vectors ["SupplDeathBft_Guaranteed"] = &SupplDeathBft_Guaranteed;
+ title_map ["SupplDeathBft_Current" ] = "Curr Suppl\nDeath\nBenefit";
+ title_map ["SupplDeathBft_Guaranteed"] = "Guar Suppl\nDeath\nBenefit";
+ format_map["SupplDeathBft_Current" ] = f1;
+ format_map["SupplDeathBft_Guaranteed"] = f1;
+
+ std::vector<double> SupplSpecAmt(Invar.TermSpecAmt);
+ vectors ["SupplSpecAmt" ] = &SupplSpecAmt;
+ title_map ["SupplSpecAmt" ] = "Suppl\nSpecified\nAmount";
+ format_map["SupplSpecAmt" ] = f1;
+
+ // [End of derived columns.]
+
+ double Composite = is_composite();
+ scalars["Composite"] = &Composite;
+
+ double NoLapse =
+ 0 != ledger_invariant_->NoLapseMinDur
+ || 0 != ledger_invariant_->NoLapseMinAge
+ ;
+ scalars["NoLapse"] = &NoLapse;
+
+ std::string LmiVersion(LMI_VERSION);
+ calendar_date prep_date;
+
+ // Skip authentication for non-interactive regression testing.
+ if(!global_settings::instance().regression_testing())
+ {
+ authenticate_system();
+ }
+ else
+ {
+ // For regression tests,
+ // - use an invariant string as version
+ // - use EffDate as date prepared
+ // in order to avoid gratuitous failures.
+ LmiVersion = "Regression testing";
+
prep_date.julian_day_number(static_cast<int>(ledger_invariant_->EffDateJdn));
+ }
+
+ strings["LmiVersion"] = &LmiVersion;
+
+ std::string PrepYear = value_cast<std::string>(prep_date.year());
+ std::string PrepMonth = month_name(prep_date.month());
+ std::string PrepDay = value_cast<std::string>(prep_date.day());
+
+ strings["PrepYear" ] = &PrepYear;
+ strings["PrepMonth"] = &PrepMonth;
+ strings["PrepDay" ] = &PrepDay;
+
+ double HasSalesLoadRefund =
+ !each_equal(ledger_invariant_->RefundableSalesLoad, 0.0);
+ double SalesLoadRefundRate0 = ledger_invariant_->RefundableSalesLoad[0];
+ double SalesLoadRefundRate1 = ledger_invariant_->RefundableSalesLoad[1];
+
+ scalars["HasSalesLoadRefund" ] = &HasSalesLoadRefund ;
+ scalars["SalesLoadRefundRate0"] = &SalesLoadRefundRate0;
+ scalars["SalesLoadRefundRate1"] = &SalesLoadRefundRate1;
+
+ double GenAcctAllocation = ledger_invariant_->GenAcctAllocation;
+ double GenAcctAllocationComplement = 1. - GenAcctAllocation;
+
+ scalars["GenAcctAllocationPercent" ] = &GenAcctAllocation;
+ scalars["GenAcctAllocationComplementPercent"] =
&GenAcctAllocationComplement;
+
+ std::string ScaleUnit = ledger_invariant_->ScaleUnit();
+ strings["ScaleUnit"] = &ScaleUnit;
+
+ double InitTotalSA =
+ ledger_invariant_->InitBaseSpecAmt
+ + ledger_invariant_->InitTermSpecAmt
+ ;
+ scalars["InitTotalSA"] = &InitTotalSA;
+
+ // Maps to hold the results of formatting numeric data.
+
+ std::unordered_map<std::string, std::string> stringscalars;
+ std::unordered_map<std::string, std::vector<std::string>> stringvectors;
+
+ stringvectors["FundNames"] = ledger_invariant_->FundNames;
+
+ // Map the data, formatting it as necessary.
+
+ // First we'll get the invariant stuff--the copy we made,
+ // along with all the stuff we plugged into it above.
+ {
+ std::string suffix = "";
+ for(auto const& j : scalars)
+ {
+ if(format_exists(j.first, suffix, format_map))
+ stringscalars[j.first + suffix] = ledger_format(*j.second,
format_map[j.first]);
+ }
+ for(auto const& j : strings)
+ {
+ stringscalars[j.first + suffix] = *j.second;
+ }
+ for(auto const& j : vectors)
+ {
+ if(format_exists(j.first, suffix, format_map))
+ stringvectors[j.first + suffix] = ledger_format(*j.second,
format_map[j.first]);
+ }
+ }
+
+// stringscalars["GuarMaxMandE"] = ledger_format(*scalars["GuarMaxMandE"],
2, true);
+// stringvectors["CorridorFactor"] =
ledger_format(*vectors["CorridorFactor"], 0, true);
+// stringscalars["InitAnnGenAcctInt_Current"] =
ledger_format(*scalars["InitAnnGenAcctInt_Current"], 0, true);
+
+ // That was the tricky part. Now it's all downhill.
+
+ for(auto const& i : ledger_map_->held())
+ {
+ std::string suffix = suffixes[i.first];
+ for(auto const& j : i.second.AllScalars)
+ {
+// scalars[j.first + suffix] = j.second;
+ if(format_exists(j.first, suffix, format_map))
+ stringscalars[j.first + suffix] = ledger_format(*j.second,
format_map[j.first]);
+ }
+ for(auto const& j : i.second.Strings)
+ {
+ strings[j.first + suffix] = j.second;
+ }
+ for(auto const& j : i.second.AllVectors)
+ {
+// vectors[j.first + suffix] = j.second;
+ if(format_exists(j.first, suffix, format_map))
+ stringvectors[j.first + suffix] = ledger_format(*j.second,
format_map[j.first]);
+ }
+ }
+
+ stringvectors["EeMode"] =
mc_e_vector_to_string_vector(ledger_invariant_->EeMode);
+ stringvectors["ErMode"] =
mc_e_vector_to_string_vector(ledger_invariant_->ErMode);
+ stringvectors["DBOpt"] =
mc_e_vector_to_string_vector(ledger_invariant_->DBOpt );
+
+// TODO ?? Here I copied some stuff from the ledger class files: the
+// parts that speak of odd members that aren't in those class's
+// maps. This may reveal incomplete or incorrect systems analysis.
+
+// Invariant
+//
+// // Special-case vectors (not <double>, or different length than others).
+// EeMode .reserve(Length);
+// ErMode .reserve(Length);
+// DBOpt .reserve(Length);
+//
+// std::vector<int> FundNumbers; [not handled yet]
+// std::vector<std::string> FundNames; [not handled yet]
+// std::vector<int> FundAllocs; [not handled yet]
+//
+// std::vector<double> InforceLives;
+//
+// // Special-case strings.
+// std::string EffDate; [furnished as PrepYear, PrepMonth, PrepDay]
+//
+// Variant
+//
+// [None of these are stored, and I think none is wanted.]
+//
+// // special cases
+// int Length;
+// mcenum_gen_basis GenBasis_;
+// mcenum_sep_basis SepBasis_;
+// bool FullyInitialized; // I.e. by Init(BasicValues const*
b)
+
+ if(ledger_invariant_->SupplementalReport)
+ {
+ std::vector<std::string> SupplementalReportColumns;
+
SupplementalReportColumns.push_back(ledger_invariant_->SupplementalReportColumn00);
+
SupplementalReportColumns.push_back(ledger_invariant_->SupplementalReportColumn01);
+
SupplementalReportColumns.push_back(ledger_invariant_->SupplementalReportColumn02);
+
SupplementalReportColumns.push_back(ledger_invariant_->SupplementalReportColumn03);
+
SupplementalReportColumns.push_back(ledger_invariant_->SupplementalReportColumn04);
+
SupplementalReportColumns.push_back(ledger_invariant_->SupplementalReportColumn05);
+
SupplementalReportColumns.push_back(ledger_invariant_->SupplementalReportColumn06);
+
SupplementalReportColumns.push_back(ledger_invariant_->SupplementalReportColumn07);
+
SupplementalReportColumns.push_back(ledger_invariant_->SupplementalReportColumn08);
+
SupplementalReportColumns.push_back(ledger_invariant_->SupplementalReportColumn09);
+
SupplementalReportColumns.push_back(ledger_invariant_->SupplementalReportColumn10);
+
SupplementalReportColumns.push_back(ledger_invariant_->SupplementalReportColumn11);
+
+ // Eventually customize the report name.
+ stringscalars["SupplementalReportTitle"] = "Supplemental Report";
+
+ std::vector<std::string> SupplementalReportColumnsTitles;
+
SupplementalReportColumnsTitles.reserve(SupplementalReportColumns.size());
+
+ for(auto const& j : SupplementalReportColumns)
+ {
+ SupplementalReportColumnsTitles.push_back(title_map[j]);
+ }
+
+ stringvectors["SupplementalReportColumnsNames"] =
std::move(SupplementalReportColumns);
+ stringvectors["SupplementalReportColumnsTitles"] =
std::move(SupplementalReportColumnsTitles);
+ }
+
+ return ledger_evaluator(std::move(stringscalars),
std::move(stringvectors));
+}
diff --git a/ledger_evaluator.hpp b/ledger_evaluator.hpp
new file mode 100644
index 0000000..07e86d8
--- /dev/null
+++ b/ledger_evaluator.hpp
@@ -0,0 +1,59 @@
+// Ledger evaluator returning values of all ledger fields.
+//
+// Copyright (C) 2017 Gregory W. Chicares.
+//
+// This program is free software; you can redistribute it and/or modify
+// it under the terms of the GNU General Public License version 2 as
+// published by the Free Software Foundation.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program; if not, write to the Free Software Foundation,
+// Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+//
+// http://savannah.nongnu.org/projects/lmi
+// email: <address@hidden>
+// snail: Chicares, 186 Belle Woods Drive, Glastonbury CT 06033, USA
+
+#ifndef ledger_evaluator_hpp
+#define ledger_evaluator_hpp
+
+#include "config.hpp"
+
+#include "so_attributes.hpp"
+
+#include <cstddef> // size_t
+#include <string>
+#include <unordered_map>
+#include <vector>
+
+/// Class allowing to retrieve the string representation of any scalar or
+/// vector stored in a ledger.
+class LMI_SO ledger_evaluator
+{
+ public:
+ std::string operator()(std::string const& scalar) const;
+ std::string operator()(std::string const& vector, std::size_t index) const;
+
+ private:
+ using all_scalars = std::unordered_map<std::string, std::string
>;
+ using all_vectors =
std::unordered_map<std::string,std::vector<std::string>>;
+
+ // Objects of this class can only be created by Ledger::make_evaluator().
+ ledger_evaluator(all_scalars&& scalars, all_vectors&& vectors)
+ :scalars_(scalars)
+ ,vectors_(vectors)
+ {
+ }
+
+ all_scalars const scalars_;
+ all_vectors const vectors_;
+
+ friend class Ledger;
+};
+
+#endif // ledger_evaluator_hpp
diff --git a/ledger_pdf.cpp b/ledger_pdf.cpp
new file mode 100644
index 0000000..48b2a75
--- /dev/null
+++ b/ledger_pdf.cpp
@@ -0,0 +1,44 @@
+// Ledger PDF generation.
+//
+// Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013,
2014, 2015, 2016, 2017 Gregory W. Chicares.
+//
+// This program is free software; you can redistribute it and/or modify
+// it under the terms of the GNU General Public License version 2 as
+// published by the Free Software Foundation.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program; if not, write to the Free Software Foundation,
+// Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+//
+// http://savannah.nongnu.org/projects/lmi
+// email: <address@hidden>
+// snail: Chicares, 186 Belle Woods Drive, Glastonbury CT 06033, USA
+
+#include "pchfile.hpp"
+
+#include "ledger_pdf.hpp"
+
+#include "configurable_settings.hpp"
+#include "ledger.hpp"
+#include "ledger_pdf_generator.hpp"
+#include "path_utility.hpp"
+
+/// Write ledger as pdf.
+
+std::string write_ledger_as_pdf(Ledger const& ledger, fs::path const& filepath)
+{
+ throw_if_interdicted(ledger);
+
+ fs::path print_dir(configurable_settings::instance().print_directory());
+ fs::path pdf_out_file = unique_filepath(print_dir / filepath, ".pdf");
+
+ auto const pdf = ledger_pdf_generator::create();
+ pdf->write(ledger, pdf_out_file);
+
+ return pdf_out_file.string();
+}
diff --git a/ledger_pdf.hpp b/ledger_pdf.hpp
new file mode 100644
index 0000000..e0b0534
--- /dev/null
+++ b/ledger_pdf.hpp
@@ -0,0 +1,35 @@
+// Ledger PDF generation.
+//
+// Copyright (C) 2017 Gregory W. Chicares.
+//
+// This program is free software; you can redistribute it and/or modify
+// it under the terms of the GNU General Public License version 2 as
+// published by the Free Software Foundation.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program; if not, write to the Free Software Foundation,
+// Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+//
+// http://savannah.nongnu.org/projects/lmi
+// email: <address@hidden>
+// snail: Chicares, 186 Belle Woods Drive, Glastonbury CT 06033, USA
+
+#ifndef ledger_pdf_hpp
+#define ledger_pdf_hpp
+
+#include "config.hpp"
+
+#include <boost/filesystem/path.hpp>
+
+#include <string>
+
+class Ledger;
+
+std::string write_ledger_as_pdf(Ledger const&, fs::path const&);
+
+#endif // ledger_pdf_hpp
diff --git a/ledger_pdf_generator.cpp b/ledger_pdf_generator.cpp
new file mode 100644
index 0000000..5061b67
--- /dev/null
+++ b/ledger_pdf_generator.cpp
@@ -0,0 +1,46 @@
+// Generate PDF files with ledger data.
+//
+// Copyright (C) 2017 Gregory W. Chicares.
+//
+// This program is free software; you can redistribute it and/or modify
+// it under the terms of the GNU General Public License version 2 as
+// published by the Free Software Foundation.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program; if not, write to the Free Software Foundation,
+// Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+//
+// http://savannah.nongnu.org/projects/lmi
+// email: <address@hidden>
+// snail: Chicares, 186 Belle Woods Drive, Glastonbury CT 06033, USA
+
+#include "pchfile.hpp"
+
+#include "ledger_pdf_generator.hpp"
+
+#include "callback.hpp"
+
+namespace
+{
+callback<ledger_pdf_generator::creator_type>
+ group_quote_pdf_generator_create_callback;
+} // Unnamed namespace.
+
+typedef ledger_pdf_generator::creator_type FunctionPointer;
+template<> FunctionPointer callback<FunctionPointer>::function_pointer_ =
nullptr;
+
+bool ledger_pdf_generator::set_creator(creator_type f)
+{
+ group_quote_pdf_generator_create_callback.initialize(f);
+ return true;
+}
+
+std::shared_ptr<ledger_pdf_generator> ledger_pdf_generator::create()
+{
+ return group_quote_pdf_generator_create_callback()();
+}
diff --git a/ledger_pdf_generator.hpp b/ledger_pdf_generator.hpp
new file mode 100644
index 0000000..3b81ff0
--- /dev/null
+++ b/ledger_pdf_generator.hpp
@@ -0,0 +1,63 @@
+// Generate PDF files with ledger data.
+//
+// Copyright (C) 2017 Gregory W. Chicares.
+//
+// This program is free software; you can redistribute it and/or modify
+// it under the terms of the GNU General Public License version 2 as
+// published by the Free Software Foundation.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program; if not, write to the Free Software Foundation,
+// Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+//
+// http://savannah.nongnu.org/projects/lmi
+// email: <address@hidden>
+// snail: Chicares, 186 Belle Woods Drive, Glastonbury CT 06033, USA
+
+#ifndef ledger_pdf_generator_hpp
+#define ledger_pdf_generator_hpp
+
+#include "config.hpp"
+
+#include "so_attributes.hpp"
+
+#include <boost/filesystem/path.hpp>
+
+#include <memory> // std::shared_ptr
+
+class Ledger;
+
+/// Abstract base class for generating PDFs with ledger data.
+///
+/// Although there is currently only a single concrete implementation of this
+/// abstract base class and no other implementations are planned, splitting the
+/// PDF generation functionality into an abstract base and the concrete derived
+/// class is still needed because the former is part of liblmi while the latter
+/// uses wxPdfDocument and other wx facilities and is only part of libskeleton.
+
+class LMI_SO ledger_pdf_generator
+{
+ public:
+ typedef std::shared_ptr<ledger_pdf_generator> (*creator_type)();
+
+ static bool set_creator(creator_type);
+ static std::shared_ptr<ledger_pdf_generator> create();
+
+ virtual ~ledger_pdf_generator() = default;
+
+ virtual void write(Ledger const& ledger, fs::path const& output) = 0;
+
+ protected:
+ ledger_pdf_generator() = default;
+
+ private:
+ ledger_pdf_generator(ledger_pdf_generator const&) = delete;
+ ledger_pdf_generator& operator=(ledger_pdf_generator const&) = delete;
+};
+
+#endif // ledger_pdf_generator_hpp
diff --git a/ledger_pdf_generator_wx.cpp b/ledger_pdf_generator_wx.cpp
new file mode 100644
index 0000000..ae6d649
--- /dev/null
+++ b/ledger_pdf_generator_wx.cpp
@@ -0,0 +1,2940 @@
+// Generate PDF files with ledger data using wxPdfDocument library.
+//
+// Copyright (C) 2017 Gregory W. Chicares.
+//
+// This program is free software; you can redistribute it and/or modify
+// it under the terms of the GNU General Public License version 2 as
+// published by the Free Software Foundation.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program; if not, write to the Free Software Foundation,
+// Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+//
+// http://savannah.nongnu.org/projects/lmi
+// email: <address@hidden>
+// snail: Chicares, 186 Belle Woods Drive, Glastonbury CT 06033, USA
+
+#include "pchfile_wx.hpp"
+
+#include "ledger_pdf_generator.hpp"
+
+#include "alert.hpp"
+#include "assert_lmi.hpp"
+#include "authenticity.hpp"
+#include "bourn_cast.hpp"
+#include "calendar_date.hpp"
+#include "data_directory.hpp" // AddDataDir()
+#include "force_linking.hpp"
+#include "html.hpp"
+#include "interpolate_string.hpp"
+#include "istream_to_string.hpp"
+#include "ledger.hpp"
+#include "ledger_evaluator.hpp"
+#include "ledger_invariant.hpp"
+#include "ledger_variant.hpp"
+#include "miscellany.hpp" // lmi_tolower()
+#include "pdf_writer_wx.hpp"
+#include "version.hpp"
+#include "wx_table_generator.hpp"
+
+#include <wx/pdfdc.h>
+
+#include <wx/image.h>
+#include <wx/log.h>
+
+#include <wx/html/m_templ.h>
+
+#include <cstdint> // SIZE_MAX
+#include <fstream>
+#include <map>
+#include <memory>
+#include <sstream>
+#include <stdexcept>
+#include <type_traits> // std::conditional
+#include <vector>
+
+LMI_FORCE_LINKING_IN_SITU(ledger_pdf_generator_wx)
+
+namespace
+{
+
+// Colour used for lines and border in the generated illustrations.
+const wxColour HIGHLIGHT_COL(0x00, 0x2f, 0x6c);
+
+// This function is also provided in <boost/algorithm/string/predicate.hpp>,
+// but it's arguably not worth adding dependency on this Boost library just for
+// this function.
+inline
+bool starts_with(std::string const& s, char const* prefix)
+{
+ return s.compare(0, strlen(prefix), prefix) == 0;
+}
+
+// Helper enums identifying the possible {Guaranteed,Current}{Zero,}
+// combinations.
+enum class base
+ {guaranteed
+ ,current
+ };
+
+enum class interest_rate
+ {zero
+ ,non_zero
+ };
+
+// And functions to retrieve their string representation.
+std::string base_suffix(base guar_or_curr)
+{
+ switch(guar_or_curr)
+ {
+ case base::guaranteed: return "Guaranteed";
+ case base::current: return "Current" ;
+ }
+ throw "Unreachable--unknown base value";
+}
+
+std::string ir_suffix(interest_rate zero_or_not)
+{
+ switch(zero_or_not)
+ {
+ case interest_rate::zero: return "Zero";
+ case interest_rate::non_zero: return "" ;
+ }
+ throw "Unreachable--unknown interest_rate value";
+}
+
+// Helper class grouping functions for dealing with interpolating strings
+// containing variable references.
+class html_interpolator
+{
+ public:
+ // Ctor takes the object used to interpolate the variables not explicitly
+ // defined using add_variable().
+ explicit html_interpolator(ledger_evaluator&& evaluator)
+ :evaluator_(evaluator)
+ {
+ }
+
+ // This function is provided to be able to delegate to it in custom
+ // interpolation functions, but usually shouldn't be called directly, just
+ // use operator() below instead.
+ std::string interpolation_func
+ (std::string const& s
+ ,interpolate_lookup_kind kind
+ ) const
+ {
+ switch(kind)
+ {
+ case interpolate_lookup_kind::variable:
+ case interpolate_lookup_kind::section:
+ return expand_html(s).as_html();
+
+ case interpolate_lookup_kind::partial:
+ return load_partial_from_file(s);
+ }
+
+ throw std::runtime_error("invalid lookup kind");
+ }
+
+ // A method which can be used to interpolate an HTML string containing
+ // references to the variables defined for this illustration. The general
+ // syntax is the same as in the global interpolate_string() function, i.e.
+ // variables are of the form "{{name}}" and section of the form
+ // "{{#name}}..{{/name}}" or "{{^name}}..{{/name}}" are also allowed and
+ // their contents is included in the expansion if and only if the variable
+ // with the given name has value "1" for the former or "0" for the latter.
+ //
+ // The variable names recognized by this function are either those defined
+ // by ledger_evaluator, i.e. scalar and vector fields of the ledger, or any
+ // variables explicitly defined by add_variable() calls.
+ html::text operator()(char const* s) const
+ {
+ return html::text::from_html
+ (interpolate_string
+ (s
+ ,[this]
+ (std::string const& str
+ ,interpolate_lookup_kind kind
+ )
+ {
+ return interpolation_func(str, kind);
+ }
+ )
+ );
+ }
+
+ html::text operator()(std::string const& s) const
+ {
+ return (*this)(s.c_str());
+ }
+
+ // Add a variable, providing either its raw text or already escaped HTML
+ // representation. Boolean values are converted to strings "0" or "1" as
+ // expected.
+ void add_variable(std::string const& name, html::text const& value)
+ {
+ vars_[name] = value;
+ }
+
+ void add_variable(std::string const& name, std::string const& value)
+ {
+ add_variable(name, html::text::from(value));
+ }
+
+ void add_variable(std::string const& name, int value)
+ {
+ std::ostringstream oss;
+ oss << value;
+ add_variable(name, oss.str());
+ }
+
+ void add_variable(std::string const& name, bool value)
+ {
+ add_variable(name, std::string(value ? "1" : "0"));
+ }
+
+ // Detect, at compile-time, mistaken attempts to add floating point
+ // variables: all those are only available from ledger_evaluator as they
+ // must be formatted correctly.
+ void add_variable(std::string const& name, double value) = delete;
+
+ // Test a boolean variable: the value must be "0" or "1", which is mapped
+ // to false or true respectively. Anything else results in an exception.
+ bool test_variable(std::string const& name) const
+ {
+ auto const z = expand_html(name).as_html();
+ return
+ z == "1" ? true
+ : z == "0" ? false
+ : throw std::runtime_error
+ ("Variable '" + name + "' has non-boolean value '" + z + "'"
+ )
+ ;
+ }
+
+ // Return the value of a single scalar variable.
+ std::string evaluate(std::string const& name) const
+ {
+ return evaluator_(name);
+ }
+
+ // Return a single value of a vector variable.
+ std::string evaluate(std::string const& name, std::size_t index) const
+ {
+ return evaluator_(name, index);
+ }
+
+ // Interpolate the contents of the given external template.
+ //
+ // This is exactly the same as interpolating "{{>template_name}}" string
+ // but a bit more convenient to use and simpler to read.
+ html::text expand_template(std::string const& template_name) const
+ {
+ return (*this)("{{>" + template_name + "}}");
+ }
+
+ private:
+ // The expansion function used with interpolate_string().
+ html::text expand_html(std::string const& s) const
+ {
+ // Check our own variables first:
+ auto const it = vars_.find(s);
+ if(it != vars_.end())
+ {
+ return it->second;
+ }
+
+ // Then look in the ledger, either as a scalar or a vector depending on
+ // whether it has "[index]" part or not.
+ if(!s.empty() && *s.rbegin() == ']')
+ {
+ auto const open_pos = s.find('[');
+ if(open_pos == std::string::npos)
+ {
+ throw std::runtime_error
+ ("Variable '" + s + "' doesn't have the expected '['"
+ );
+ }
+
+ char* stop = nullptr;
+ auto const index = std::strtoul(s.c_str() + open_pos + 1, &stop,
10);
+
+ // Conversion must have stopped at the closing bracket character
+ // and also check for overflow (notice that index == SIZE_MAX
+ // doesn't, in theory, need to indicate overflow, but in practice
+ // we're never going to have valid indices close to this number).
+ if(stop != s.c_str() + s.length() - 1 || index >= SIZE_MAX)
+ {
+ throw std::runtime_error
+ ("Index of vector variable '" + s + "' is not a valid
number"
+ );
+ }
+
+ // Cast below is valid because of the check for overflow above.
+ return html::text::from
+ (evaluator_
+ (s.substr(0, open_pos)
+ ,static_cast<std::size_t>(index)
+ )
+ );
+ }
+
+ return html::text::from(evaluator_(s));
+ }
+
+ std::string load_partial_from_file(std::string const& file) const
+ {
+ std::ifstream ifs(AddDataDir(file + ".mst"));
+ if(!ifs)
+ {
+ alarum()
+ << "Template file \""
+ << file
+ << ".mst\" not found."
+ << std::flush
+ ;
+ }
+ std::string partial;
+ istream_to_string(ifs, partial);
+ return partial;
+ }
+
+ // Object used for variables expansion.
+ ledger_evaluator const evaluator_;
+
+ // Variables defined for all pages of this illustration.
+ std::map<std::string, html::text> vars_;
+};
+
+// A slightly specialized table generator for the tables used in the
+// illustrations.
+class illustration_table_generator : public wx_table_generator
+{
+ public:
+ static int const rows_per_group = 5;
+
+ explicit illustration_table_generator(pdf_writer_wx& writer)
+ :wx_table_generator
+ (writer.dc()
+ ,writer.get_horz_margin()
+ ,writer.get_page_width()
+ )
+ {
+ use_condensed_style();
+ align_right();
+ }
+
+ // Return the amount of vertical space taken by separator lines in the
+ // table headers.
+ int get_separator_line_height() const
+ {
+ // This is completely arbitrary and chosen just because it seems to
+ // look well.
+ return row_height() / 2;
+ }
+};
+
+// A helper mix-in class for pages using tables which is also reused by the
+// custom wxHtmlCell showing a table.
+//
+// Derived classes must provide get_table_columns() and may also override
+// should_show_column() to hide some of these columns dynamically and then can
+// use create_table_generator() to obtain the generator object that can be used
+// to render a table with the specified columns.
+class using_illustration_table
+{
+ protected:
+ // Description of a single table column.
+ struct illustration_table_column
+ {
+ std::string const variable_name;
+ std::string const label;
+ std::string const widest_text;
+ };
+
+ using illustration_table_columns = std::vector<illustration_table_column>;
+
+ // Must be overridden to return the description of the table columns.
+ virtual illustration_table_columns const& get_table_columns() const = 0;
+
+ // May be overridden to return false if the given column shouldn't be shown
+ // for the specific ledger values (currently used to exclude individual
+ // columns from composite illustrations).
+ virtual bool should_show_column(Ledger const& ledger, int column) const
+ {
+ stifle_warning_for_unused_value(ledger);
+ stifle_warning_for_unused_value(column);
+ return true;
+ }
+
+ // Useful helper for creating the table generator using the columns defined
+ // by the separate (and simpler to implement) get_table_columns() pure
+ // virtual method.
+ illustration_table_generator create_table_generator
+ (Ledger const& ledger
+ ,pdf_writer_wx& writer
+ ) const
+ {
+ // Set the smaller font used for all tables before creating the table
+ // generator which uses the DC font for its measurements.
+ auto& dc = writer.dc();
+ auto font = dc.GetFont();
+ font.SetPointSize(9);
+ dc.SetFont(font);
+
+ illustration_table_generator table(writer);
+
+ // But set the highlight colour for drawing separator lines after
+ // creating it to override its default pen.
+ dc.SetPen(HIGHLIGHT_COL);
+
+ int column = 0;
+ for(auto const& i : get_table_columns())
+ {
+ std::string label;
+ if(should_show_column(ledger, column++))
+ {
+ label = i.label;
+ }
+ //else: Leave the label empty to avoid showing the column.
+
+ table.add_column(label, i.widest_text);
+ }
+
+ return table;
+ }
+};
+
+// Base class for our custom HTML cells providing a way to pass them
+// information about the PDF document being generated and the ledger used to
+// generate it.
+class html_cell_for_pdf_output : public wxHtmlCell
+{
+ public:
+ // Before using this class a pdf_context_setter object needs to be
+ // instantiated (and remain alive for as long as this class is used).
+ class pdf_context_setter
+ {
+ public:
+ // References passed to the ctor must have lifetime greater than that
+ // of this object itself.
+ explicit pdf_context_setter
+ (Ledger const& ledger
+ ,pdf_writer_wx& writer
+ ,html_interpolator const& interpolate_html
+ )
+ {
+ html_cell_for_pdf_output::pdf_context_for_html_output.set
+ (&ledger
+ ,&writer
+ ,&interpolate_html
+ );
+ }
+
+ ~pdf_context_setter()
+ {
+ html_cell_for_pdf_output::pdf_context_for_html_output.set
+ (nullptr
+ ,nullptr
+ ,nullptr
+ );
+ }
+ };
+
+ protected:
+ // This is ugly, but we have to use a global variable to make pdf_writer_wx
+ // and wxDC objects used by the main code accessible to this cell class,
+ // there is no way to pass them as parameters through wxHTML machinery.
+ //
+ // To at least make it a little bit safer to deal with this, the variable
+ // itself is private and a public pdf_context_setter class is provided to
+ // actually set it.
+ class pdf_context
+ {
+ public:
+ void set
+ (Ledger const* ledger
+ ,pdf_writer_wx* writer
+ ,html_interpolator const* interpolate_html
+ )
+ {
+ ledger_ = ledger;
+ writer_ = writer;
+ interpolate_html_ = interpolate_html;
+ }
+
+ Ledger const& ledger() const
+ {
+ LMI_ASSERT(ledger_);
+ return *ledger_;
+ }
+
+ pdf_writer_wx& writer() const
+ {
+ LMI_ASSERT(writer_);
+ return *writer_;
+ }
+
+ html_interpolator const& interpolate_html() const
+ {
+ LMI_ASSERT(interpolate_html_);
+ return *interpolate_html_;
+ }
+
+ private:
+ Ledger const* ledger_ = nullptr;
+ pdf_writer_wx* writer_ = nullptr;
+ html_interpolator const* interpolate_html_ = nullptr;
+ };
+
+ // Small helper to check that we're using the expected DC and, also, acting
+ // as a sink for the never used parameters of Draw().
+ void draw_check_precondition
+ (wxDC& dc
+ ,int view_y1
+ ,int view_y2
+ ,wxHtmlRenderingInfo& info
+ )
+ {
+ // The DC passed to this function is supposed to be the same as the one
+ // associated with the writer we will use for rendering, but check that
+ // this is really so in order to avoid unexpectedly drawing the table
+ // on something else.
+ LMI_ASSERT(&dc == &pdf_context_for_html_output.writer().dc());
+
+ // There is no need to optimize drawing by restricting it to the
+ // currently shown positions, we always render the cell entirely.
+ stifle_warning_for_unused_value(view_y1);
+ stifle_warning_for_unused_value(view_y2);
+
+ // We don't care about rendering state as we don't support interactive
+ // selection anyhow.
+ stifle_warning_for_unused_value(info);
+ }
+
+ static pdf_context pdf_context_for_html_output;
+
+ friend pdf_context_setter;
+};
+
+html_cell_for_pdf_output::pdf_context
+html_cell_for_pdf_output::pdf_context_for_html_output;
+
+// Define scaffolding for a custom HTML "scaled_image" tag which must be used
+// instead of the standard "a" in order to allow specifying the scaling factor
+// that we want to use for the image in the PDF. Unfortunately this can't be
+// achieved by simply using "width" and/or "height" attributes of the "a" tag
+// because their values can only be integers which is not precise enough to
+// avoid (slightly but noticeably) distorting the image due to the aspect ratio
+// being not quite right.
+
+class scaled_image_cell : public html_cell_for_pdf_output
+{
+ public:
+ scaled_image_cell
+ (wxImage const& image
+ ,wxString const& src
+ ,double scale_factor
+ )
+ :image_(image)
+ ,src_(src)
+ ,scale_factor_(scale_factor)
+ {
+ m_Width = wxRound(image.GetWidth () / scale_factor);
+ m_Height = wxRound(image.GetHeight() / scale_factor);
+ }
+
+ // Override the base class method to actually render the image.
+ void Draw
+ (wxDC& dc
+ ,int x
+ ,int y
+ ,int view_y1
+ ,int view_y2
+ ,wxHtmlRenderingInfo& info
+ ) override
+ {
+ draw_check_precondition(dc, view_y1, view_y2, info);
+
+ auto& writer = pdf_context_for_html_output.writer();
+
+ x += m_PosX;
+
+ int pos_y = y + m_PosY;
+ writer.output_image(image_, src_.utf8_str(), scale_factor_, x, &pos_y);
+ }
+
+ private:
+ wxImage const image_;
+ wxString const src_;
+ double const scale_factor_;
+};
+
+TAG_HANDLER_BEGIN(scaled_image, "SCALED_IMAGE")
+ TAG_HANDLER_PROC(tag)
+ {
+ wxString src;
+ if (!tag.GetParamAsString("SRC", &src))
+ {
+ throw std::runtime_error
+ ("missing mandatory \"src\" attribute of \"scaled_image\" tag"
+ );
+ }
+
+ // The scale factor is optional.
+ double scale_factor = 1.;
+
+ // But if it is given, we currently specify its inverse in HTML just
+ // because it so happens that for the scale factors we use the inverse
+ // can be expressed exactly in decimal notation, while the factor
+ // itself can't. In principle, the converse could also happen and we
+ // might add support for "factor" attribute too in this case. Or we
+ // could use separate "numerator" and "denominator" attributes. But for
+ // now implement just the bare minimum of what we need.
+ wxString inv_factor_str;
+ if (tag.GetParamAsString("INV_FACTOR", &inv_factor_str))
+ {
+ double inv_factor = 0.;
+ if (!inv_factor_str.ToCDouble(&inv_factor) || inv_factor == 0.)
+ {
+ throw std::runtime_error
+ ( "invalid value for \"inv_factor\" attribute of "
+ "\"scaled_image\" tag: \""
+ + inv_factor_str.ToStdString()
+ + "\""
+ );
+ }
+
+ scale_factor = 1./inv_factor;
+ }
+
+ wxImage image;
+ // Disable error logging, we'll simply ignore the tag if the image is
+ // not present.
+ {
+ wxLogNull noLog;
+ image.LoadFile(src);
+ }
+
+ if (image.IsOk())
+ {
+ m_WParser->GetContainer()->InsertCell
+ (new scaled_image_cell(image, src, scale_factor)
+ );
+ }
+
+ // This tag isn't supposed to have any inner contents, so return true
+ // to not even try parsing it.
+ return true;
+ }
+TAG_HANDLER_END(scaled_image)
+
+class pdf_illustration;
+
+// Base class for all logical illustration pages.
+//
+// A single logical page may result in multiple physical pages of output, e.g.
+// if it contains a table not fitting on one page, but mostly these page
+// objects correspond to a single physical page of the resulting illustration.
+class page
+{
+ public:
+ page() = default;
+
+ // Pages are not value-like objects, so prohibit copying them.
+ page(page const&) = delete;
+ page& operator=(page const&) = delete;
+
+ // Make base class dtor virtual.
+ virtual ~page() = default;
+
+ // Associate the illustration object using this page with it.
+ //
+ // This object is not passed as a ctor argument because it would be
+ // redundant, instead it is associated with the page when it's added to an
+ // illustration. This method is supposed to be called only once and only by
+ // pdf_illustration this page is being added to.
+ void illustration(pdf_illustration const& illustration)
+ {
+ LMI_ASSERT(!illustration_);
+
+ illustration_ = &illustration;
+ }
+
+ // Called before rendering any pages to prepare for doing this, e.g. by
+ // computing the number of pages needed.
+ //
+ // This method must not draw anything on the wxDC, it is provided only for
+ // measurement purposes.
+ virtual void pre_render
+ (Ledger const& ledger
+ ,pdf_writer_wx& writer
+ ,html_interpolator const& interpolate_html
+ )
+ {
+ stifle_warning_for_unused_value(ledger);
+ stifle_warning_for_unused_value(writer);
+ stifle_warning_for_unused_value(interpolate_html);
+ }
+
+ // Render this page contents.
+ virtual void render
+ (Ledger const& ledger
+ ,pdf_writer_wx& writer
+ ,html_interpolator const& interpolate_html
+ ) = 0;
+
+ protected:
+ // Helper method for rendering the contents of the given external template,
+ // which is expected to be found in the file with the provided name and
+ // ".mst" extension in the data directory.
+ //
+ // Return the height of the page contents.
+ int render_page_template
+ (std::string const& template_name
+ ,pdf_writer_wx& writer
+ ,html_interpolator const& interpolate_html
+ )
+ {
+ return writer.output_html
+ (writer.get_horz_margin()
+ ,writer.get_vert_margin()
+ ,writer.get_page_width()
+ ,interpolate_html.expand_template(template_name)
+ );
+ }
+
+ // The associated illustration, which will be non-null by the time our
+ // virtual methods such as pre_render() and render() are called.
+ pdf_illustration const* illustration_ = nullptr;
+};
+
+// Base class for the different kinds of illustrations.
+//
+// This object contains pages, added to it using its add() method, as well as
+// illustration-global data registered as variables with html_interpolator and
+// so available for the pages when expanding the external templates defining
+// their contents.
+class pdf_illustration : protected html_interpolator
+{
+ public:
+ pdf_illustration(Ledger const& ledger
+ ,fs::path const& output
+ )
+ :html_interpolator(ledger.make_evaluator())
+ ,writer_(output.string(), wxPORTRAIT, &html_font_sizes)
+ ,ledger_(ledger)
+ {
+ init_variables();
+ }
+
+ // Make base class dtor virtual.
+ virtual ~pdf_illustration() = default;
+
+ // Add a page.
+ //
+ // This is a template just in order to save on writing std::make_unique<>()
+ // in the code using it to make it slightly shorter.
+ template<typename T, typename... Args>
+ void add(Args&&... args)
+ {
+ auto page = std::make_unique<T>(std::forward<Args>(args)...);
+ page->illustration(*this);
+ pages_.emplace_back(std::move(page));
+ }
+
+ // Render all pages.
+ void render_all()
+ {
+ html_cell_for_pdf_output::pdf_context_setter
+ set_pdf_context(ledger_, writer_, *this);
+
+ for(auto const& page : pages_)
+ {
+ page->pre_render(ledger_, writer_, *this);
+ }
+
+ bool first = true;
+ for(auto const& page : pages_)
+ {
+ if(first)
+ {
+ // We shouldn't start a new page before the very first one.
+ first = false;
+ }
+ else
+ {
+ // Do start a new physical page before rendering all the
+ // subsequent pages (notice that a page is also free to call
+ // StartPage() from its render()).
+ writer_.dc().StartPage();
+ }
+
+ page->render(ledger_, writer_, *this);
+ }
+ }
+
+ // Methods to be implemented by the derived classes to indicate which
+ // templates should be used for the upper (above the separating line) and
+ // the lower parts of the footer. The upper template name may be empty if
+ // it is not used at all.
+ //
+ // Notice that the upper footer template name can be overridden at the page
+ // level, the methods here define the default for all illustration pages.
+ //
+ // These methods are used by the pages deriving from page_with_footer.
+ virtual std::string get_upper_footer_template_name() const = 0;
+ virtual std::string get_lower_footer_template_name() const = 0;
+
+ protected:
+ // Explicitly retrieve the base class.
+ html_interpolator const& get_interpolator() const {return *this;}
+
+ // Helper for abbreviating a string to at most the given length (in bytes).
+ static std::string abbreviate_if_necessary(std::string s, size_t len)
+ {
+ if(s.length() > len && len > 3)
+ {
+ s.replace(len - 3, std::string::npos, "...");
+ }
+
+ return s;
+ }
+
+ // Helper for creating abbreviated variables in the derived classes: such
+ // variables have the name based on the name of the original variable with
+ // "Abbrev" and "len" appended to it and their value is at most "len" bytes
+ // long.
+ void add_abbreviated_variable(std::string const& var, size_t len)
+ {
+ add_variable
+ (var + "Abbrev" + std::to_string(len)
+ ,abbreviate_if_necessary(evaluate(var), len)
+ );
+ }
+
+ private:
+ // Define variables that can be used when interpolating pages contents.
+ void init_variables()
+ {
+ // The variables defined here are used by all, or at least more than
+ // one, illustration kinds. Variables only used in the templates of a
+ // single illustration type should be defined in the corresponding
+ // derived pdf_illustration_xxx class instead.
+
+ add_variable
+ ("date_prepared"
+ , html::text::from(evaluate("PrepMonth"))
+ + html::text::nbsp()
+ + html::text::from(evaluate("PrepDay"))
+ + html::text::from(", ")
+ + html::text::from(evaluate("PrepYear"))
+ );
+
+ auto indent = html::text::nbsp();
+ add_variable("Space1", indent);
+
+ indent += indent;
+ add_variable("Space2", indent);
+
+ indent += indent;
+ add_variable("Space4", indent);
+
+ indent += indent;
+ add_variable("Space8", indent);
+
+ indent += indent;
+ add_variable("Space16", indent);
+
+ indent += indent;
+ add_variable("Space32", indent);
+
+ indent += indent;
+ add_variable("Space64", indent);
+
+ auto const& invar = ledger_.GetLedgerInvariant();
+
+ add_abbreviated_variable("CorpName", 60);
+ add_abbreviated_variable("Insured1", 30);
+
+ // Define the variables needed by contract_numbers template.
+ add_variable
+ ("HasMasterContract"
+ ,!invar.MasterContractNumber.empty()
+ );
+ add_variable
+ ("HasPolicyNumber"
+ ,!invar.ContractNumber.empty()
+ );
+
+ size_t const full_abbrev_length = 30;
+ add_abbreviated_variable("MasterContractNumber", full_abbrev_length);
+ add_abbreviated_variable("MasterContractNumber", full_abbrev_length /
2);
+ add_abbreviated_variable("ContractNumber", full_abbrev_length);
+ add_abbreviated_variable("ContractNumber", full_abbrev_length / 2);
+
+ add_variable
+ ("HasComplianceTrackingNumber"
+ ,expand_template("imprimatur")
+ .as_html().find_first_not_of(" \n")
+ != std::string::npos
+ );
+
+ add_variable
+ ("HasScaleUnit"
+ ,!invar.ScaleUnit().empty()
+ );
+
+ add_variable
+ ("DefnLifeInsIsGPT"
+ ,invar.DefnLifeIns == "GPT"
+ );
+
+ add_variable
+ ("MecYearPlus1"
+ ,bourn_cast<int>(invar.MecYear) + 1
+ );
+
+ add_variable
+ ("UWTypeIsMedical"
+ ,invar.UWType == "Medical"
+ );
+
+ add_variable
+ ("UWClassIsRated"
+ ,invar.UWClass == "Rated"
+ );
+
+ auto const& policy_name = invar.PolicyLegalName;
+ add_variable
+ ("GroupCarveout"
+ ,policy_name == "Group Flexible Premium Adjustable Life Insurance
Certificate"
+ );
+
+ auto const& state_abbrev = invar.GetStatePostalAbbrev();
+ add_variable
+ ("StateIsCarolina"
+ ,state_abbrev == "NC" || state_abbrev == "SC"
+ );
+
+ add_variable
+ ("StateIsMaryland"
+ ,state_abbrev == "MD"
+ );
+ }
+
+ // This array stores the non-default font sizes that are used to make it
+ // simpler to replicate the existing illustrations.
+ static std::array<int, 7> const html_font_sizes;
+
+ // Writer object used for the page metrics and higher level functions.
+ pdf_writer_wx writer_;
+
+ // Source of the data.
+ Ledger const& ledger_;
+
+ // All the pages of this illustration.
+ std::vector<std::unique_ptr<page>> pages_;
+};
+
+std::array<int, 7> const pdf_illustration::html_font_sizes
+ {
+ { 8
+ , 9
+ ,10
+ ,12
+ ,14
+ ,18
+ ,20
+ }
+ };
+
+// Cover page used by several different illustration kinds.
+class cover_page : public page
+{
+ public:
+ void render
+ (Ledger const& /* ledger */
+ ,pdf_writer_wx& writer
+ ,html_interpolator const& interpolate_html
+ ) override
+ {
+ int const height_contents = render_page_template
+ ("cover"
+ ,writer
+ ,interpolate_html
+ );
+
+ // There is no way to draw a border around the page contents in wxHTML
+ // currently, so do it manually.
+ auto& dc = writer.dc();
+
+ dc.SetPen(wxPen(HIGHLIGHT_COL, 2));
+ dc.SetBrush(*wxTRANSPARENT_BRUSH);
+
+ dc.DrawRectangle
+ (writer.get_horz_margin()
+ ,writer.get_vert_margin()
+ ,writer.get_page_width()
+ ,height_contents
+ );
+ }
+};
+
+// Base class for all pages with a footer.
+class page_with_footer : public page
+{
+ public:
+ // Override pre_render() to compute footer_top_ which is needed in the
+ // derived classes overridden get_extra_pages_needed().
+ void pre_render
+ (Ledger const& /* ledger */
+ ,pdf_writer_wx& writer
+ ,html_interpolator const& interpolate_html
+ ) override
+ {
+ auto const frame_horz_margin = writer.get_horz_margin();
+ auto const frame_width = writer.get_page_width();
+
+ // We implicitly assume here that get_footer_lower_html() result
+ // doesn't materially depend on the exact value of the page number as
+ // we don't know its definitive value here yet. In theory, this doesn't
+ // need to be true, e.g. we may later discover that 10 pages are needed
+ // instead of 9 and the extra digit might result in a line wrapping on
+ // a new line and this increasing the footer height, but in practice
+ // this doesn't risk happening and taking into account this possibility
+ // wouldn't be simple at all, so just ignore this possibility.
+ auto footer_height = writer.output_html
+ (frame_horz_margin
+ ,0
+ ,frame_width
+ ,get_footer_lower_html(interpolate_html)
+ ,e_output_measure_only
+ );
+
+ auto const& upper_template = get_upper_footer_template_name();
+ if(!upper_template.empty())
+ {
+ footer_height += writer.output_html
+ (frame_horz_margin
+ ,0
+ ,frame_width
+ ,interpolate_html.expand_template(upper_template)
+ ,e_output_measure_only
+ );
+
+ // Leave a gap between the upper part of the footer and the main
+ // page contents to separate them in absence of a separator line
+ // which delimits the lower part.
+ footer_height += writer.dc().GetCharHeight();
+ }
+
+ footer_top_ = writer.get_page_bottom() - footer_height;
+ }
+
+ void render
+ (Ledger const& /* ledger */
+ ,pdf_writer_wx& writer
+ ,html_interpolator const& interpolate_html
+ ) override
+ {
+ auto const frame_horz_margin = writer.get_horz_margin();
+ auto const frame_width = writer.get_page_width();
+
+ auto& dc = writer.dc();
+
+ auto y = footer_top_;
+
+ auto const& upper_template = get_upper_footer_template_name();
+ if(!upper_template.empty())
+ {
+ y += dc.GetCharHeight();
+
+ y += writer.output_html
+ (frame_horz_margin
+ ,y
+ ,frame_width
+ ,interpolate_html.expand_template(upper_template)
+ );
+ }
+
+ writer.output_html
+ (frame_horz_margin
+ ,y
+ ,frame_width
+ ,get_footer_lower_html(interpolate_html)
+ );
+
+ dc.SetPen(HIGHLIGHT_COL);
+ dc.DrawLine
+ (frame_horz_margin
+ ,y
+ ,frame_width + frame_horz_margin
+ ,y
+ );
+ }
+
+ protected:
+ // Helper for the derived pages to get the vertical position of the footer.
+ // Notice that it can only be used after calling our pre_render() method
+ // as this is where it is computed.
+ int get_footer_top() const
+ {
+ LMI_ASSERT(footer_top_ != 0);
+
+ return footer_top_;
+ }
+
+ private:
+ // Method to be overridden in the base class which should actually return
+ // the page number or equivalent string (e.g. "Appendix").
+ virtual std::string get_page_number() const = 0;
+
+ // This method forwards to the illustration by default, but can be
+ // overridden to define a page-specific footer if necessary.
+ virtual std::string get_upper_footer_template_name() const
+ {
+ return illustration_->get_upper_footer_template_name();
+ }
+
+ // This method uses get_page_number() and returns the HTML wrapping it
+ // and other fixed information appearing in the lower part of the footer.
+ html::text get_footer_lower_html(html_interpolator const&
interpolate_html) const
+ {
+ auto const page_number_str = get_page_number();
+
+ auto const templ = illustration_->get_lower_footer_template_name();
+
+ // Use our own interpolation function to handle the special
+ // "page_number" variable that is replaced with the actual
+ // (possibly dynamic) page number.
+ return html::text::from_html
+ (interpolate_string
+ (("{{>" + templ + "}}").c_str()
+ ,[page_number_str, interpolate_html]
+ (std::string const& s
+ ,interpolate_lookup_kind kind
+ ) -> std::string
+ {
+ if(s == "page_number")
+ {
+ return page_number_str;
+ }
+
+ return interpolate_html.interpolation_func(s, kind);
+ }
+ )
+ );
+ }
+
+ int footer_top_ = 0;
+};
+
+// Base class for attachment pages.
+class attachment_page : public page_with_footer
+{
+ private:
+ std::string get_page_number() const override
+ {
+ return "Attachment";
+ }
+};
+
+// Base class for all pages showing the page number in the footer.
+//
+// In addition to actually providing page_with_footer with the correct string
+// to show in the footer, this class implicitly handles the page count by
+// incrementing it whenever a new object of this class is pre-rendered.
+class numbered_page : public page_with_footer
+{
+ public:
+ // Must be called before creating the first numbered page.
+ static void start_numbering()
+ {
+ last_page_number_ = 0;
+ }
+
+ numbered_page()
+ {
+ // This assert would fail if start_numbering() hadn't been called
+ // before creating a numbered page, as it should be.
+ LMI_ASSERT(last_page_number_ >= 0);
+ }
+
+ void pre_render
+ (Ledger const& ledger
+ ,pdf_writer_wx& writer
+ ,html_interpolator const& interpolate_html
+ ) override
+ {
+ page_with_footer::pre_render(ledger, writer, interpolate_html);
+
+ this_page_number_ = ++last_page_number_;
+
+ extra_pages_ = get_extra_pages_needed
+ (ledger
+ ,writer
+ ,interpolate_html
+ );
+
+ LMI_ASSERT(extra_pages_ >= 0);
+
+ last_page_number_ += extra_pages_;
+ }
+
+ ~numbered_page() override
+ {
+ // Check that next_page() was called the expected number of times.
+ // Unfortunately we can't use LMI_ASSERT() in the (noexcept) dtor, so
+ // use warning() instead.
+ if(extra_pages_)
+ {
+ warning()
+ << "Logic error: "
+ << extra_pages_
+ << " missing extra pages."
+ << LMI_FLUSH
+ ;
+ }
+ }
+
+ protected:
+ void next_page(pdf_writer_wx& writer)
+ {
+ // This method may only be called if we had reserved enough physical
+ // pages for this logical pages by overriding get_extra_pages_needed().
+ LMI_ASSERT(extra_pages_ > 0);
+
+ writer.dc().StartPage();
+
+ this_page_number_++;
+ extra_pages_--;
+ }
+
+ private:
+ // Derived classes may override this method if they may need more than one
+ // physical page to show their contents.
+ virtual int get_extra_pages_needed
+ (Ledger const& ledger
+ ,pdf_writer_wx& writer
+ ,html_interpolator const& interpolate_html
+ ) const
+ {
+ stifle_warning_for_unused_value(ledger);
+ stifle_warning_for_unused_value(writer);
+ stifle_warning_for_unused_value(interpolate_html);
+
+ return 0;
+ }
+
+ std::string get_page_number() const override
+ {
+ std::ostringstream oss;
+ oss << "Page " << this_page_number_ << " of " << last_page_number_;
+ return oss.str();
+ }
+
+ static int last_page_number_;
+ int this_page_number_ = 0;
+ int extra_pages_ = 0;
+};
+
+// Initial value is invalid, use start_numbering() to change it.
+int numbered_page::last_page_number_ = -1;
+
+// Simplest possible page which is entirely defined by its external template
+// whose name must be specified when constructing it.
+class standard_page : public numbered_page
+{
+ public:
+ // Accept only string literals as template names, there should be no need
+ // to use anything else.
+ template<int N>
+ explicit standard_page(char const (&page_template_name)[N])
+ :page_template_name_(page_template_name)
+ {
+ }
+
+ void render
+ (Ledger const& ledger
+ ,pdf_writer_wx& writer
+ ,html_interpolator const& interpolate_html
+ ) override
+ {
+ numbered_page::render(ledger, writer, interpolate_html);
+
+ render_page_template(page_template_name_, writer, interpolate_html);
+ }
+
+ private:
+ char const* const page_template_name_;
+};
+
+// Helper classes used to show the numeric summary table. The approach used
+// here is to define a custom HTML tag (<numeric_summary_table>) and use the
+// existing illustration_table_generator to replace it with the actual table
+// when rendering.
+//
+// Notice that we currently make the simplifying assumption that this table is
+// always short enough so that everything fits on the same page as it would be
+// much more complicated to handle page breaks in the table in the middle of a
+// page (page_with_tabular_report below handles them only for the table at the
+// bottom of the page, after all the other contents, and this is already more
+// complicated and can't be done with just a custom HTML tag as we do it here).
+
+// An HTML cell showing the contents of the numeric summary table.
+class numeric_summary_table_cell
+ :public html_cell_for_pdf_output
+ ,private using_illustration_table
+{
+ public:
+ numeric_summary_table_cell()
+ {
+ m_Height = render_or_measure(0, e_output_measure_only);
+ }
+
+ // Override the base class method to actually render the table.
+ void Draw
+ (wxDC& dc
+ ,int x
+ ,int y
+ ,int view_y1
+ ,int view_y2
+ ,wxHtmlRenderingInfo& info
+ ) override
+ {
+ draw_check_precondition(dc, view_y1, view_y2, info);
+
+ // We ignore the horizontal coordinate which is always 0 for this cell
+ // anyhow.
+ stifle_warning_for_unused_value(x);
+
+ render_or_measure(y + m_PosY, e_output_normal);
+ }
+
+ private:
+ enum
+ {column_policy_year
+ ,column_premium_outlay
+ ,column_guar_account_value
+ ,column_guar_cash_surr_value
+ ,column_guar_death_benefit
+ ,column_separator_guar_non_guar
+ ,column_mid_account_value
+ ,column_mid_cash_surr_value
+ ,column_mid_death_benefit
+ ,column_separator_mid_curr
+ ,column_curr_account_value
+ ,column_curr_cash_surr_value
+ ,column_curr_death_benefit
+ ,column_max
+ };
+
+ illustration_table_columns const& get_table_columns() const override
+ {
+ static illustration_table_columns const columns =
+ {{ "PolicyYear" , "Policy\nYear" , "999" }
+ ,{ "GrossPmt" , "Premium\nOutlay" , "999,999" }
+ ,{ "AcctVal_Guaranteed" , "Account\nValue" , "999,999" }
+ ,{ "CSVNet_Guaranteed" , "Cash Surr\nValue" , "999,999" }
+ ,{ "EOYDeathBft_Guaranteed" , "Death\nBenefit" , "9,999,999" }
+ ,{ "" , " " , "-" }
+ ,{ "AcctVal_Midpoint" , "Account\nValue" , "999,999" }
+ ,{ "CSVNet_Midpoint" , "Cash Surr\nValue" , "999,999" }
+ ,{ "EOYDeathBft_Midpoint" , "Death\nBenefit" , "9,999,999" }
+ ,{ "" , " " , "-" }
+ ,{ "AcctVal_Current" , "Account\nValue" , "999,999" }
+ ,{ "CSVNet_Current" , "Cash Surr\nValue" , "999,999" }
+ ,{ "EOYDeathBft_Current" , "Death\nBenefit" , "9,999,999" }
+ };
+
+ return columns;
+ }
+
+ int render_or_measure(int pos_y, enum_output_mode output_mode)
+ {
+ auto const& ledger = pdf_context_for_html_output.ledger();
+ auto& writer = pdf_context_for_html_output.writer();
+
+ illustration_table_generator
+ table{create_table_generator(ledger, writer)};
+
+ // Output multiple rows of headers.
+
+ // Make a copy because we want pos_y to be modified only once, not
+ // twice, by both output_super_header() calls.
+ auto y_copy = pos_y;
+ table.output_super_header
+ ("Guaranteed Values"
+ ,column_guar_account_value
+ ,column_separator_guar_non_guar
+ ,&y_copy
+ ,output_mode
+ );
+ table.output_super_header
+ ("Non-Guaranteed Values"
+ ,column_mid_account_value
+ ,column_max
+ ,&pos_y
+ ,output_mode
+ );
+
+ pos_y += table.get_separator_line_height();
+ table.output_horz_separator
+ (column_guar_account_value
+ ,column_separator_guar_non_guar
+ ,pos_y
+ ,output_mode
+ );
+ table.output_horz_separator
+ (column_mid_account_value
+ ,column_max
+ ,pos_y
+ ,output_mode
+ );
+
+ y_copy = pos_y;
+ table.output_super_header
+ ("Midpoint Values"
+ ,column_mid_account_value
+ ,column_separator_mid_curr
+ ,&y_copy
+ ,output_mode
+ );
+
+ table.output_super_header
+ ("Current Values"
+ ,column_curr_account_value
+ ,column_max
+ ,&pos_y
+ ,output_mode
+ );
+
+ pos_y += table.get_separator_line_height();
+ table.output_horz_separator
+ (column_mid_account_value
+ ,column_separator_mid_curr
+ ,pos_y
+ ,output_mode
+ );
+
+ table.output_horz_separator
+ (column_curr_account_value
+ ,column_max
+ ,pos_y
+ ,output_mode
+ );
+
+ table.output_header(&pos_y, output_mode);
+
+ pos_y += table.get_separator_line_height();
+ table.output_horz_separator(0, column_max, pos_y, output_mode);
+
+ // And now the table values themselves.
+ auto const& columns = get_table_columns();
+ std::vector<std::string> values(columns.size());
+
+ auto const& invar = ledger.GetLedgerInvariant();
+ auto const& interpolate_html =
pdf_context_for_html_output.interpolate_html();
+
+ int const year_max =
pdf_context_for_html_output.ledger().GetMaxLength();
+ int const age_last = 70;
+ std::array<int, 4> const summary_years =
+ {{5, 10, 20, age_last - bourn_cast<int>(invar.Age)}
+ };
+ for(auto const& year : summary_years)
+ {
+ // Skip row if it doesn't exist. For instance, if the issue
+ // age is 85 and the contract remains in force until age 100,
+ // then there is no twentieth duration and no age-70 row.
+ if(!(0 < year && year <= year_max))
+ {
+ continue;
+ }
+
+ // Last row, showing the values for "Age 70" normally, needs to be
+ // handled specially.
+ bool const is_last_row = &year == &summary_years.back();
+
+ // For composite ledgers, "Age" doesn't make sense and so this row
+ // should be just skipped for them.
+ if(is_last_row && ledger.is_composite())
+ {
+ continue;
+ }
+
+ switch(output_mode)
+ {
+ case e_output_measure_only:
+ pos_y += table.row_height();
+ break;
+
+ case e_output_normal:
+ for(std::size_t col = 0; col < columns.size(); ++col)
+ {
+ std::string const variable_name =
columns[col].variable_name;
+
+ // According to regulations, we need to replace the
+ // policy year in the last row with the age.
+ if(col == column_policy_year)
+ {
+ if(is_last_row)
+ {
+ std::ostringstream oss;
+ oss << "Age " << age_last;
+ values[col] = oss.str();
+ continue;
+ }
+ }
+
+ // Special hack for the dummy columns whose value is
always
+ // empty as it's used only as separator.
+ values[col] = variable_name.empty()
+ ? std::string{}
+ : interpolate_html.evaluate(variable_name, year -
1)
+ ;
+ }
+
+ table.output_row(&pos_y, values.data());
+ break;
+ }
+ }
+
+ return pos_y;
+ }
+};
+
+// Custom tag which is replaced by the numeric summary table.
+TAG_HANDLER_BEGIN(numeric_summary_table, "NUMERIC_SUMMARY_TABLE")
+ TAG_HANDLER_PROC(tag)
+ {
+ // The tag argument would be useful if we defined any parameters for
+ // it, but currently we don't.
+ stifle_warning_for_unused_value(tag);
+
+ m_WParser->GetContainer()->InsertCell(new
numeric_summary_table_cell());
+
+ // This tag isn't supposed to have any inner contents, so return true
+ // to not even try parsing it.
+ return true;
+ }
+TAG_HANDLER_END(numeric_summary_table)
+
+// In wxWidgets versions prior to 3.1.1, there is an extra semicolon at the end
+// of TAGS_MODULE_BEGIN() expansion resulting in a warning with -pedantic used
+// by lmi, so suppress this warning here (this could be removed once 3.1.1 is
+// required).
+wxGCC_WARNING_SUPPRESS(pedantic)
+
+TAGS_MODULE_BEGIN(lmi_illustration)
+ TAGS_MODULE_ADD(scaled_image)
+ TAGS_MODULE_ADD(numeric_summary_table)
+TAGS_MODULE_END(lmi_illustration)
+
+wxGCC_WARNING_RESTORE(pedantic)
+
+// Numeric summary page appears twice, once as a normal page and once as an
+// attachment, with the only difference being that the base class is different,
+// so make it a template to avoid duplicating the code.
+
+// Just a helper alias.
+template<bool is_attachment>
+using numbered_or_attachment_base = typename std::conditional
+ <is_attachment
+ ,attachment_page
+ ,numbered_page
+ >::type;
+
+template<bool is_attachment>
+class reg_numeric_summary_or_attachment_page
+ : public numbered_or_attachment_base<is_attachment>
+{
+ public:
+ void render
+ (Ledger const& ledger
+ ,pdf_writer_wx& writer
+ ,html_interpolator const& interpolate_html
+ ) override
+ {
+ numbered_or_attachment_base<is_attachment>::render
+ (ledger
+ ,writer
+ ,interpolate_html
+ );
+
+ this->render_page_template
+ ("reg_numeric_summary"
+ ,writer
+ ,interpolate_html
+ );
+ }
+};
+
+// Helper base class for pages showing a table displaying values for all
+// contract years after some fixed content.
+class page_with_tabular_report
+ :public numbered_page
+ ,protected using_illustration_table
+{
+ public:
+ void render
+ (Ledger const& ledger
+ ,pdf_writer_wx& writer
+ ,html_interpolator const& interpolate_html
+ ) override
+ {
+ numbered_page::render(ledger, writer, interpolate_html);
+
+ illustration_table_generator
+ table{create_table_generator(ledger, writer)};
+
+ auto const& columns = get_table_columns();
+
+ // Just some cached values used inside the loop below.
+ auto const row_height = table.row_height();
+ auto const page_bottom = get_footer_top();
+ auto const rows_per_group =
illustration_table_generator::rows_per_group;
+ std::vector<std::string> values(columns.size());
+
+ // The table may need several pages, loop over them.
+ int const year_max = ledger.GetMaxLength();
+ for(int year = 0; year < year_max; ++year)
+ {
+ int pos_y = render_or_measure_fixed_page_part
+ (table
+ ,writer
+ ,interpolate_html
+ ,e_output_normal
+ );
+
+ for(; year < year_max; ++year)
+ {
+ for(std::size_t col = 0; col < columns.size(); ++col)
+ {
+ std::string const variable_name =
columns[col].variable_name;
+
+ // Special hack for the dummy columns used in some reports,
+ // whose value is always empty as it's used only as
+ // separator.
+ values[col] = variable_name.empty()
+ ? std::string{}
+ : interpolate_html.evaluate(variable_name, year)
+ ;
+ }
+
+ table.output_row(&pos_y, values.data());
+
+ if((year + 1) % rows_per_group == 0)
+ {
+ // We need a group break.
+ pos_y += row_height;
+
+ // And possibly a page break, which will be necessary if
we don't
+ // have enough space for another full group because we
don't want
+ // to have page breaks in the middle of a group.
+ if(pos_y >= page_bottom - rows_per_group*row_height)
+ {
+ next_page(writer);
+ numbered_page::render(ledger, writer,
interpolate_html);
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ protected:
+ // Must be overridden to return the template containing the fixed page
part.
+ virtual std::string get_fixed_page_contents_template_name() const = 0;
+
+ // May be overridden to render (only if output_mode is e_output_normal)
+ // the extra headers just above the regular table headers.
+ //
+ // If this function does anything, it must show the first super-header at
+ // pos_y and update it to account for the added lines. The base class
+ // version does nothing.
+ virtual void render_or_measure_extra_headers
+ (illustration_table_generator& table
+ ,html_interpolator const& interpolate_html
+ ,int* pos_y
+ ,enum_output_mode output_mode
+ ) const
+ {
+ stifle_warning_for_unused_value(table);
+ stifle_warning_for_unused_value(interpolate_html);
+ stifle_warning_for_unused_value(pos_y);
+ stifle_warning_for_unused_value(output_mode);
+ }
+
+ private:
+ // Render (only if output_mode is e_output_normal) the fixed page part and
+ // (in any case) return the vertical coordinate of its bottom, where the
+ // tabular report starts.
+ int render_or_measure_fixed_page_part
+ (illustration_table_generator& table
+ ,pdf_writer_wx& writer
+ ,html_interpolator const& interpolate_html
+ ,enum_output_mode output_mode
+ ) const
+ {
+ int pos_y = writer.get_vert_margin();
+
+ pos_y += writer.output_html
+ (writer.get_horz_margin()
+ ,pos_y
+ ,writer.get_page_width()
+ ,interpolate_html.expand_template
+ (get_fixed_page_contents_template_name()
+ )
+ ,output_mode
+ );
+
+ render_or_measure_extra_headers
+ (table
+ ,interpolate_html
+ ,&pos_y
+ ,output_mode
+ );
+
+ table.output_header(&pos_y, output_mode);
+
+ pos_y += table.get_separator_line_height();
+ table.output_horz_separator
+ (0
+ ,table.columns_count()
+ ,pos_y
+ ,output_mode
+ );
+
+ return pos_y;
+ }
+
+ // Override the base class method as the table may overflow onto the next
+ // page(s).
+ int get_extra_pages_needed
+ (Ledger const& ledger
+ ,pdf_writer_wx& writer
+ ,html_interpolator const& interpolate_html
+ ) const override
+ {
+ illustration_table_generator
+ table{create_table_generator(ledger, writer)};
+
+ int const pos_y = render_or_measure_fixed_page_part
+ (table
+ ,writer
+ ,interpolate_html
+ ,e_output_measure_only
+ );
+
+ int const rows_per_page = (get_footer_top() - pos_y) /
table.row_height();
+
+ int const rows_per_group =
illustration_table_generator::rows_per_group;
+
+ if(rows_per_page < rows_per_group)
+ {
+ // We can't afford to continue in this case as we can never output
+ // the table as the template simply doesn't leave enough space for
+ // it on the page.
+ throw std::runtime_error("no space left for tabular report");
+ }
+
+ // Each group actually takes rows_per_group+1 rows because of the
+ // separator row between groups, hence the second +1, but there is no
+ // need for the separator after the last group, hence the first +1.
+ int const groups_per_page = (rows_per_page + 1) / (rows_per_group + 1);
+
+ // But we are actually interested in the number of years per page and
+ // not the number of groups.
+ int const years_per_page = groups_per_page * rows_per_group;
+
+ // Finally determine how many pages we need to show all the years.
+ return ledger.GetMaxLength() / years_per_page;
+ }
+};
+
+class reg_tabular_detail_page : public page_with_tabular_report
+{
+ private:
+ enum
+ {column_policy_year
+ ,column_end_of_year_age
+ ,column_premium_outlay
+ ,column_guar_account_value
+ ,column_guar_cash_surr_value
+ ,column_guar_death_benefit
+ ,column_dummy_separator
+ ,column_curr_account_value
+ ,column_curr_cash_surr_value
+ ,column_curr_death_benefit
+ ,column_max
+ };
+
+ std::string get_fixed_page_contents_template_name() const override
+ {
+ return "reg_tabular_details";
+ }
+
+ std::string get_upper_footer_template_name() const override
+ {
+ return "reg_footer_disclaimer";
+ }
+
+ void render_or_measure_extra_headers
+ (illustration_table_generator& table
+ ,html_interpolator const& interpolate_html
+ ,int* pos_y
+ ,enum_output_mode output_mode
+ ) const override
+ {
+ stifle_warning_for_unused_value(interpolate_html);
+
+ // Make a copy because we want the real pos_y to be modified only once,
+ // not twice, by both output_super_header() calls.
+ auto pos_y_copy = *pos_y;
+ table.output_super_header
+ ("Guaranteed Values"
+ ,column_guar_account_value
+ ,column_dummy_separator
+ ,&pos_y_copy
+ ,output_mode
+ );
+ table.output_super_header
+ ("Non-Guaranteed Values"
+ ,column_curr_account_value
+ ,column_max
+ ,pos_y
+ ,output_mode
+ );
+
+ *pos_y += table.get_separator_line_height();
+ table.output_horz_separator
+ (column_guar_account_value
+ ,column_dummy_separator
+ ,*pos_y
+ ,output_mode
+ );
+ table.output_horz_separator
+ (column_curr_account_value
+ ,column_max
+ ,*pos_y
+ ,output_mode
+ );
+ }
+
+ illustration_table_columns const& get_table_columns() const override
+ {
+ static illustration_table_columns const columns =
+ {{ "PolicyYear" , "Policy\nYear" , "999" }
+ ,{ "AttainedAge" , "End of\nYear Age" , "999" }
+ ,{ "GrossPmt" , "Premium\nOutlay" , "999,999" }
+ ,{ "AcctVal_Guaranteed" , "Account\nValue" , "999,999" }
+ ,{ "CSVNet_Guaranteed" , "Cash Surr\nValue" , "999,999" }
+ ,{ "EOYDeathBft_Guaranteed" , "Death\nBenefit" , "9,999,999" }
+ ,{ "" , " " , "----" }
+ ,{ "AcctVal_Current" , "Account\nValue" , "999,999" }
+ ,{ "CSVNet_Current" , "Cash Surr\nValue" , "999,999" }
+ ,{ "EOYDeathBft_Current" , "Death\nBenefit" , "9,999,999" }
+ };
+
+ return columns;
+ }
+
+ bool should_show_column(Ledger const& ledger, int column) const override
+ {
+ // One column should be hidden for composite ledgers.
+ return column != column_end_of_year_age || !ledger.is_composite();
+ }
+};
+
+class reg_tabular_detail2_page : public page_with_tabular_report
+{
+ private:
+ enum
+ {column_policy_year
+ ,column_end_of_year_age
+ ,column_ill_crediting_rate
+ ,column_selected_face_amount
+ ,column_max
+ };
+
+ std::string get_fixed_page_contents_template_name() const override
+ {
+ return "reg_tabular_details2";
+ }
+
+ std::string get_upper_footer_template_name() const override
+ {
+ return "reg_footer_disclaimer";
+ }
+
+ illustration_table_columns const& get_table_columns() const override
+ {
+ static illustration_table_columns const columns =
+ {{ "PolicyYear" , "Policy\nYear" ,
"999" }
+ ,{ "AttainedAge" , "End of\nYear Age" ,
"999" }
+ ,{ "AnnGAIntRate_Current", "Illustrated\nCrediting Rate",
"99.99%" }
+ ,{ "SpecAmt" , "Selected\nFace Amount" ,
"999,000,000" }
+ };
+
+ return columns;
+ }
+
+ bool should_show_column(Ledger const& ledger, int column) const override
+ {
+ // One column should be hidden for composite ledgers.
+ return column != column_end_of_year_age || !ledger.is_composite();
+ }
+};
+
+// Class for pages showing supplemental report after the fixed template
+// contents. It can be either used directly or further derived from, e.g. to
+// override some of its inherited virtual methods such as
+// get_upper_footer_template_name() as done below.
+class standard_supplemental_report : public page_with_tabular_report
+{
+ public:
+ explicit standard_supplemental_report
+ (html_interpolator const& interpolate_html
+ ,std::string const& page_template
+ )
+ :columns_(build_columns(interpolate_html))
+ ,page_template_(page_template)
+ {
+ }
+
+ private:
+ illustration_table_columns const& get_table_columns() const override
+ {
+ return columns_;
+ }
+
+ std::string get_fixed_page_contents_template_name() const override
+ {
+ return page_template_;
+ }
+
+ // Helper function used by the ctor to initialize the const columns_ field.
+ illustration_table_columns build_columns
+ (html_interpolator const& interpolate_html
+ )
+ {
+ constexpr std::size_t max_columns = 12;
+ std::string const empty_column_name("[none]");
+
+ illustration_table_columns columns;
+ for(std::size_t i = 0; i < max_columns; ++i)
+ {
+ auto name =
interpolate_html.evaluate("SupplementalReportColumnsNames", i);
+ if(name != empty_column_name)
+ {
+ // We currently don't have the field width information for
+ // arbitrary fields, so use fixed width that should be
+ // sufficient for almost all of them.
+ columns.emplace_back
+ (illustration_table_column
+ {std::move(name)
+
,interpolate_html.evaluate("SupplementalReportColumnsTitles", i)
+ ,"999,999"
+ }
+ );
+ }
+ }
+
+ return columns;
+ }
+
+ illustration_table_columns const columns_ ;
+ std::string const page_template_;
+};
+
+class reg_supplemental_report : public standard_supplemental_report
+{
+ public:
+ explicit reg_supplemental_report(html_interpolator const& interpolate_html)
+ :standard_supplemental_report(interpolate_html, "reg_supp_report")
+ {
+ }
+
+ private:
+ std::string get_upper_footer_template_name() const override
+ {
+ return "reg_footer_disclaimer";
+ }
+};
+
+// Regular illustration.
+class pdf_illustration_regular : public pdf_illustration
+{
+ public:
+ pdf_illustration_regular(Ledger const& ledger
+ ,fs::path const& output
+ )
+ :pdf_illustration(ledger, output)
+ {
+ auto const& invar = ledger.GetLedgerInvariant();
+ auto const& policy_name = invar.PolicyLegalName;
+ auto const& state_abbrev = invar.GetStatePostalAbbrev();
+
+ // Define variables specific to this illustration which doesn't use the
+ // standard 60/30 lengths for whatever reason.
+ add_abbreviated_variable("CorpName", 50);
+ add_abbreviated_variable("Insured1", 50);
+
+ add_variable
+ ("ModifiedSinglePremium"
+ ,starts_with(policy_name, "Single") && state_abbrev == "MA"
+ );
+
+ add_variable
+ ("ModifiedSinglePremium0"
+ ,starts_with(policy_name, "Modified")
+ );
+
+ add_variable
+ ("ModifiedSinglePremiumOrModifiedSinglePremium0"
+ , test_variable("ModifiedSinglePremium")
+ ||test_variable("ModifiedSinglePremium0")
+ );
+
+ add_variable
+ ("SinglePremium"
+ ,starts_with(policy_name, "Single") || starts_with(policy_name,
"Modified")
+ );
+
+ add_variable
+ ("GroupExperienceRating"
+ ,policy_name == "Group Flexible Premium Adjustable Life Insurance
Policy"
+ );
+
+ // Variable representing the premium payment frequency with the
+ // appropriate indefinite article preceding it, e.g. "an annual" or "a
+ // monthly".
+ auto const er_mode = invar.ErMode[0].str();
+ if(!er_mode.empty())
+ {
+ auto const er_mode_first = lmi_tolower(er_mode[0]);
+ add_variable
+ ("ErModeLCWithArticle"
+ ,(strchr("aeiou", er_mode_first) ? "an" : "a") +
er_mode.substr(1)
+ );
+ }
+
+ add_variable
+ ("HasProducerCity"
+ ,invar.ProducerCity != "0"
+ );
+
+ add_variable
+ ("HasInterestDisclaimer"
+ ,!invar.InterestDisclaimer.empty()
+ );
+
+ add_variable
+ ("HasGuarPrem"
+ ,invar.GuarPrem != 0
+ );
+
+ add_variable
+ ("StateIsIllinois"
+ ,state_abbrev == "IL"
+ );
+
+ add_variable
+ ("StateIsTexas"
+ ,state_abbrev == "TX"
+ );
+
+ add_variable
+ ("StateIsIllinoisOrTexas"
+ ,state_abbrev == "IL" || state_abbrev == "TX"
+ );
+
+ add_variable
+ ("UltimateInterestRate"
+ ,evaluate("AnnGAIntRate_Current", invar.InforceYear + 1)
+ );
+
+ auto const max_duration = invar.EndtAge - invar.Age;
+ auto const lapse_year_guaruanteed = ledger.GetGuarFull().LapseYear;
+ auto const lapse_year_midpoint = ledger.GetMdptFull().LapseYear;
+ auto const lapse_year_current = ledger.GetCurrFull().LapseYear;
+
+ add_variable
+ ("LapseYear_Guaranteed_LT_MaxDuration"
+ ,lapse_year_guaruanteed < max_duration
+ );
+
+ add_variable
+ ("LapseYear_Guaranteed_Plus1"
+ ,bourn_cast<int>(lapse_year_guaruanteed) + 1
+ );
+
+ add_variable
+ ("LapseYear_Midpoint_LT_MaxDuration"
+ ,lapse_year_midpoint < max_duration
+ );
+
+ add_variable
+ ("LapseYear_Midpoint_Plus1"
+ ,bourn_cast<int>(lapse_year_midpoint) + 1
+ );
+
+ add_variable
+ ("LapseYear_Current_LT_MaxDuration"
+ ,lapse_year_current < max_duration
+ );
+
+ add_variable
+ ("LapseYear_Current_Plus1"
+ ,bourn_cast<int>(lapse_year_current) + 1
+ );
+
+ // Add all the pages.
+ add<cover_page>();
+ numbered_page::start_numbering();
+ add<standard_page>("reg_narr_summary");
+ add<standard_page>("reg_narr_summary2");
+ add<standard_page>("reg_column_headings");
+ if(!invar.IsInforce)
+ {
+ add<reg_numeric_summary_or_attachment_page<false>>();
+ }
+ add<reg_tabular_detail_page>();
+ add<reg_tabular_detail2_page>();
+ if(invar.SupplementalReport)
+ {
+ add<reg_supplemental_report>(get_interpolator());
+ }
+ if(!invar.IsInforce)
+ {
+ add<reg_numeric_summary_or_attachment_page<true>>();
+ }
+ }
+
+ std::string get_upper_footer_template_name() const override
+ { return {}; }
+ std::string get_lower_footer_template_name() const override
+ { return "reg_footer"; }
+};
+
+// Common base class for basic illustration pages using the same columns in
+// both NASD and private group placement illustrations.
+class page_with_basic_tabular_report : public page_with_tabular_report
+{
+ private:
+ // This method must be overridden to return the text of the super-header
+ // used for all pairs of "cash surrogate value" and "death benefit"
+ // columns. The return value is subject to HTML interpolation and so may
+ // contain {{variables}} and also can be multiline but, if so, it must have
+ // the same number of lines for all input arguments.
+ //
+ // The base and interest_rate arguments can be used to construct the full
+ // name of the variable appropriate for the current column pair, with the
+ // help of base_suffix() and ir_suffix() functions.
+ virtual std::string get_two_column_header
+ (base guar_or_curr
+ ,interest_rate zero_or_not
+ ) const = 0;
+
+ enum
+ {column_policy_year
+ ,column_end_of_year_age
+ ,column_premium_outlay
+ ,column_guar0_cash_surr_value
+ ,column_guar0_death_benefit
+ ,column_separator_guar0_guar
+ ,column_guar_cash_surr_value
+ ,column_guar_death_benefit
+ ,column_separator_guar_curr0
+ ,column_curr0_cash_surr_value
+ ,column_curr0_death_benefit
+ ,column_separator_curr0_curr
+ ,column_curr_cash_surr_value
+ ,column_curr_death_benefit
+ ,column_max
+ };
+
+ illustration_table_columns const& get_table_columns() const override
+ {
+ static illustration_table_columns const columns =
+ {{ "PolicyYear" , "Policy\nYear" , "999"
}
+ ,{ "AttainedAge" , "End of\nYear Age" , "999"
}
+ ,{ "GrossPmt" , "Premium\nOutlay" , "999,999"
}
+ ,{ "CSVNet_GuaranteedZero" , "Cash Surr\nValue" , "999,999"
}
+ ,{ "EOYDeathBft_GuaranteedZero" , "Death\nBenefit" , "9,999,999"
}
+ ,{ "" , " " , "-"
}
+ ,{ "CSVNet_Guaranteed" , "Cash Surr\nValue" , "999,999"
}
+ ,{ "EOYDeathBft_Guaranteed" , "Death\nBenefit" , "9,999,999"
}
+ ,{ "" , " " , "-"
}
+ ,{ "CSVNet_CurrentZero" , "Cash Surr\nValue" , "999,999"
}
+ ,{ "EOYDeathBft_CurrentZero" , "Death\nBenefit" , "9,999,999"
}
+ ,{ "" , " " , "-"
}
+ ,{ "CSVNet_Current" , "Cash Surr\nValue" , "999,999"
}
+ ,{ "EOYDeathBft_Current" , "Death\nBenefit" , "9,999,999"
}
+ };
+
+ return columns;
+ }
+
+ bool should_show_column(Ledger const& ledger, int column) const override
+ {
+ // One column should be hidden for composite ledgers.
+ return column != column_end_of_year_age || !ledger.is_composite();
+ }
+
+ void render_or_measure_extra_headers
+ (illustration_table_generator& table
+ ,html_interpolator const& interpolate_html
+ ,int* pos_y
+ ,enum_output_mode output_mode
+ ) const override
+ {
+ // Output the first super header row.
+
+ auto pos_y_copy = *pos_y;
+ table.output_super_header
+ ("Using guaranteed charges"
+ ,column_guar0_cash_surr_value
+ ,column_separator_guar_curr0
+ ,pos_y
+ ,output_mode
+ );
+
+ *pos_y = pos_y_copy;
+ table.output_super_header
+ ("Using current charges"
+ ,column_curr0_cash_surr_value
+ ,column_max
+ ,pos_y
+ ,output_mode
+ );
+
+ *pos_y += table.get_separator_line_height();
+ table.output_horz_separator
+ (column_guar0_cash_surr_value
+ ,column_separator_guar_curr0
+ ,*pos_y
+ ,output_mode
+ );
+ table.output_horz_separator
+ (column_curr0_cash_surr_value
+ ,column_max
+ ,*pos_y
+ ,output_mode
+ );
+
+ // Output the second super header row which is composed of three
+ // physical lines.
+
+ // This function outputs all lines of a single header, corresponding to
+ // the "Guaranteed" or "Current", "Zero" or not, column and returns the
+ // vertical position below the header.
+ auto const output_two_column_super_header = [=,&table]
+ (base guar_or_curr
+ ,interest_rate zero_or_not
+ ,std::size_t begin_column
+ ) -> int
+ {
+ std::size_t end_column = begin_column + 2;
+ LMI_ASSERT(end_column <= column_max);
+
+ auto y = *pos_y;
+
+ auto const header = get_two_column_header
+ (guar_or_curr
+ ,zero_or_not
+ );
+ table.output_super_header
+ (interpolate_html(header).as_html()
+ ,begin_column
+ ,end_column
+ ,&y
+ ,output_mode
+ );
+
+ y += table.get_separator_line_height();
+ table.output_horz_separator
+ (begin_column
+ ,end_column
+ ,y
+ ,output_mode
+ );
+
+ return y;
+ };
+
+ output_two_column_super_header
+ (base::guaranteed
+ ,interest_rate::zero
+ ,column_guar0_cash_surr_value
+ );
+
+ output_two_column_super_header
+ (base::guaranteed
+ ,interest_rate::non_zero
+ ,column_guar_cash_surr_value
+ );
+
+ output_two_column_super_header
+ (base::current
+ ,interest_rate::zero
+ ,column_curr0_cash_surr_value
+ );
+
+ *pos_y = output_two_column_super_header
+ (base::current
+ ,interest_rate::non_zero
+ ,column_curr_cash_surr_value
+ );
+ }
+};
+
+class nasd_basic : public page_with_basic_tabular_report
+{
+ private:
+ std::string get_fixed_page_contents_template_name() const override
+ {
+ return "nasd_basic";
+ }
+
+ std::string get_two_column_header
+ (base guar_or_curr
+ ,interest_rate zero_or_not
+ ) const override
+ {
+ std::ostringstream oss;
+ oss
+ << "{{InitAnnSepAcctGrossInt_"
+ << base_suffix(guar_or_curr)
+ << ir_suffix(zero_or_not)
+ << "}} "
+ << "Assumed Sep Acct\n"
+ << "Gross Rate* "
+ << "({{InitAnnSepAcctNetInt_"
+ << base_suffix(guar_or_curr)
+ << ir_suffix(zero_or_not)
+ << "}} net)\n"
+ << "{{InitAnnGenAcctInt_"
+ << base_suffix(guar_or_curr)
+ << "}} GPA rate"
+ ;
+ return oss.str();
+ }
+};
+
+class nasd_supplemental : public page_with_tabular_report
+{
+ private:
+ enum
+ {column_policy_year
+ ,column_end_of_year_age
+ ,column_er_gross_payment
+ ,column_ee_gross_payment
+ ,column_premium_outlay
+ ,column_admin_charge
+ ,column_premium_tax_load
+ ,column_dac_tax_load
+ ,column_er_min_premium
+ ,column_ee_min_premium
+ ,column_net_premium
+ ,column_cost_of_insurance_charges
+ ,column_curr_account_value
+ ,column_curr_cash_surr_value
+ ,column_curr_death_benefit
+ ,column_max
+ };
+
+ std::string get_fixed_page_contents_template_name() const override
+ {
+ return "nasd_supp";
+ }
+
+ illustration_table_columns const& get_table_columns() const override
+ {
+ static illustration_table_columns const columns =
+ {{ "PolicyYear" , "Policy\nYear" ,
"999" }
+ ,{ "AttainedAge" , "End of\nYear Age" ,
"999" }
+ ,{ "ErGrossPmt" , "ER Gross\nPayment" ,
"999,999" }
+ ,{ "EeGrossPmt" , "EE Gross\nPayment" ,
"999,999" }
+ ,{ "GrossPmt" , "Premium\nOutlay" ,
"999,999" }
+ ,{ "PolicyFee_Current" , "Admin\nCharge" ,
"999,999" }
+ ,{ "PremTaxLoad_Current" , "Premium\nTax Load" ,
"999,999" }
+ ,{ "DacTaxLoad_Current" , "DAC\nTax Load" ,
"999,999" }
+ ,{ "ErModalMinimumPremium", "ER Modal\nMinimum\nPremium" ,
"999,999" }
+ ,{ "EeModalMinimumPremium", "EE Modal\nMinimum\nPremium" ,
"999,999" }
+ ,{ "NetPmt_Current" , "Net\nPremium" ,
"999,999" }
+ ,{ "COICharge_Current" , "Cost of\nInsurance\nCharges",
"999,999" }
+ ,{ "AcctVal_Current" , "Current\nAccount\nValue" ,
"999,999" }
+ ,{ "CSVNet_Current" , "Current\nCash Surr\nValue" ,
"999,999" }
+ ,{ "EOYDeathBft_Current" , "Current\nDeath\nBenefit" ,
"9,999,999" }
+ };
+
+ return columns;
+ }
+
+ bool should_show_column(Ledger const& ledger, int column) const override
+ {
+ auto const& invar = ledger.GetLedgerInvariant();
+
+ // The supplemental page in NASD illustrations exists in two versions:
+ // default one and one with split premiums. Hide columns that are not
+ // needed for the current illustration.
+ switch(column)
+ {
+ case column_end_of_year_age:
+ // This column doesn't make sense for composite ledgers.
+ return !ledger.is_composite();
+
+ case column_admin_charge:
+ case column_premium_tax_load:
+ case column_dac_tax_load:
+ // These columns only appear in non-split premiums case.
+ return invar.SplitMinPrem == 0.;
+
+ case column_er_gross_payment:
+ case column_ee_gross_payment:
+ case column_er_min_premium:
+ case column_ee_min_premium:
+ // While those only appear in split premiums case.
+ return invar.SplitMinPrem == 1.;
+
+ case column_policy_year:
+ case column_premium_outlay:
+ case column_net_premium:
+ case column_cost_of_insurance_charges:
+ case column_curr_account_value:
+ case column_curr_cash_surr_value:
+ case column_curr_death_benefit:
+ case column_max:
+ // These columns are common to both cases and never hidden.
+ break;
+ }
+
+ return true;
+ }
+};
+
+class nasd_assumption_detail : public page_with_tabular_report
+{
+ private:
+ enum
+ {column_policy_year
+ ,column_end_of_year_age
+ ,column_sep_acct_crediting_rate
+ ,column_gen_acct_crediting_rate
+ ,column_m_and_e
+ ,column_ee_payment_mode
+ ,column_er_payment_mode
+ ,column_assumed_loan_interest
+ ,column_max
+ };
+
+ std::string get_fixed_page_contents_template_name() const override
+ {
+ return "nasd_assumption_detail";
+ }
+
+ illustration_table_columns const& get_table_columns() const override
+ {
+ static illustration_table_columns const columns =
+ {{ "PolicyYear" , "Policy\nYear" ,
"999" }
+ ,{ "AttainedAge" , "End of\nYear Age" ,
"999" }
+ ,{ "AnnSAIntRate_Current", "Sep Acct Net\nInv Rate" ,
"99.99%" }
+ ,{ "AnnGAIntRate_Current", "Gen Acct\nCurrent Rate" ,
"99.99%" }
+ ,{ "CurrMandE" , "M&E" ,
"99.99%" }
+ ,{ "EeMode" , "Indiv\nPmt Mode" ,
"Semiannual" }
+ ,{ "ErMode" , "Corp\nPmt Mode" ,
"Semiannual" }
+ ,{ "InitAnnLoanDueRate" , "Assumed\nLoan Interest" ,
"99.99%" }
+ };
+
+ return columns;
+ }
+
+ // Notice that there is no need to override should_show_column() in this
+ // class as this page is not included in composite illustrations and hence
+ // all of its columns, including the "AttainedAge" one, are always shown.
+};
+
+// NASD illustration.
+class pdf_illustration_nasd : public pdf_illustration
+{
+ public:
+ pdf_illustration_nasd
+ (Ledger const& ledger
+ ,fs::path const& output
+ )
+ :pdf_illustration(ledger, output)
+ {
+ auto const& invar = ledger.GetLedgerInvariant();
+
+ // Define variables specific to this illustration.
+ if(!invar.ContractName.empty())
+ {
+ std::string s = invar.ContractName;
+ for(auto& c : s)
+ {
+ c = lmi_tolower(c);
+ }
+ s[0] = lmi_toupper(s[0]);
+
+ add_variable("ContractNameCap", s);
+ }
+
+ add_variable
+ ("UWTypeIsGuaranteedIssueInTexasWithFootnote"
+ ,invar.UWType == "Guaranteed issue"
+ );
+
+ add_variable
+ ("HasTermOrSupplSpecAmt"
+ ,test_variable("HasTerm") || test_variable("HasSupplSpecAmt")
+ );
+
+ auto const& state_abbrev = invar.GetStatePostalAbbrev();
+ add_variable
+ ("StateIsNewYork"
+ ,state_abbrev == "NY"
+ );
+
+ // Add all the pages.
+ add<cover_page>();
+ numbered_page::start_numbering();
+ add<nasd_basic>();
+ add<nasd_supplemental>();
+ add<standard_page>("nasd_column_headings");
+ add<standard_page>("nasd_notes1");
+ add<standard_page>("nasd_notes2");
+ if(!ledger.is_composite())
+ {
+ add<nasd_assumption_detail>();
+ }
+ if(invar.SupplementalReport)
+ {
+ add<standard_supplemental_report>
+ (get_interpolator()
+ ,"nasd_supp_report"
+ );
+ }
+ }
+
+ std::string get_upper_footer_template_name() const override
+ {
+ return "nasd_footer_upper";
+ }
+
+ std::string get_lower_footer_template_name() const override
+ {
+ return "nasd_footer_lower";
+ }
+};
+
+// Basic illustration page of the private group placement illustration.
+class reg_d_group_basic : public page_with_basic_tabular_report
+{
+ private:
+ std::string get_fixed_page_contents_template_name() const override
+ {
+ return "reg_d_group_basic";
+ }
+
+ std::string get_two_column_header
+ (base guar_or_curr
+ ,interest_rate zero_or_not
+ ) const override
+ {
+ std::ostringstream oss;
+ oss
+ << "{{InitAnnSepAcctGrossInt_"
+ << base_suffix(guar_or_curr)
+ << ir_suffix(zero_or_not)
+ << "}} "
+ << "Hypothetical Gross\n"
+ << "Return ({{InitAnnSepAcctNetInt_"
+ << base_suffix(guar_or_curr)
+ << ir_suffix(zero_or_not)
+ << "}} net)"
+ ;
+ return oss.str();
+ }
+};
+
+// Private group placement illustration.
+class pdf_illustration_reg_d_group : public pdf_illustration
+{
+ public:
+ pdf_illustration_reg_d_group
+ (Ledger const& ledger
+ ,fs::path const& output
+ )
+ :pdf_illustration(ledger, output)
+ {
+ // Define variables specific to this illustration.
+ auto const& invar = ledger.GetLedgerInvariant();
+
+ add_variable
+ ("MecYearIs0"
+ ,invar.MecYear == 0
+ );
+
+ // Add all the pages.
+ add<cover_page>();
+ numbered_page::start_numbering();
+ add<reg_d_group_basic>();
+ add<standard_page>("reg_d_group_column_headings");
+ add<standard_page>("reg_d_group_narr_summary");
+ add<standard_page>("reg_d_group_narr_summary2");
+ if(invar.SupplementalReport)
+ {
+ add<standard_supplemental_report>
+ (get_interpolator()
+ ,"reg_d_group_supp_report"
+ );
+ }
+ }
+
+ std::string get_upper_footer_template_name() const override
+ {
+ return "reg_d_group_footer_upper";
+ }
+
+ std::string get_lower_footer_template_name() const override
+ {
+ return "reg_d_group_footer_lower";
+ }
+};
+
+// This page exists in two almost identical versions, one using guaranteed and
+// the other one using current values, use a base class to share the common
+// parts.
+class reg_d_individual_irr_base : public page_with_tabular_report
+{
+ private:
+ enum
+ {column_policy_year
+ ,column_end_of_year_age
+ ,column_premium_outlay
+ ,column_zero_cash_surr_value
+ ,column_zero_death_benefit
+ ,column_zero_irr_surr_value
+ ,column_zero_irr_death_benefit
+ ,column_separator
+ ,column_nonzero_cash_surr_value
+ ,column_nonzero_death_benefit
+ ,column_nonzero_irr_surr_value
+ ,column_nonzero_irr_death_benefit
+ ,column_max
+ };
+
+ // Must be overridden to return the base being used.
+ virtual base get_base() const = 0;
+
+ bool should_show_column(Ledger const& ledger, int column) const override
+ {
+ // One column should be hidden for composite ledgers.
+ return column != column_end_of_year_age || !ledger.is_composite();
+ }
+
+ void render_or_measure_extra_headers
+ (illustration_table_generator& table
+ ,html_interpolator const& interpolate_html
+ ,int* pos_y
+ ,enum_output_mode output_mode
+ ) const override
+ {
+ std::ostringstream header_zero;
+ header_zero
+ << "{{InitAnnSepAcctGrossInt_"
+ << base_suffix(get_base())
+ << ir_suffix(interest_rate::zero)
+ << "}} Hypothetical Rate of\n"
+ << "Return*"
+ ;
+
+ auto pos_y_copy = *pos_y;
+ table.output_super_header
+ (interpolate_html(header_zero.str()).as_html()
+ ,column_zero_cash_surr_value
+ ,column_zero_irr_surr_value
+ ,pos_y
+ ,output_mode
+ );
+
+ std::ostringstream header_nonzero;
+ header_nonzero
+ << "{{InitAnnSepAcctGrossInt_"
+ << base_suffix(get_base())
+ << ir_suffix(interest_rate::non_zero)
+ << "}} Hypothetical Rate of\n"
+ << "Return*"
+ ;
+
+ *pos_y = pos_y_copy;
+ table.output_super_header
+ (interpolate_html(header_nonzero.str()).as_html()
+ ,column_nonzero_cash_surr_value
+ ,column_nonzero_irr_surr_value
+ ,pos_y
+ ,output_mode
+ );
+
+ *pos_y += table.get_separator_line_height();
+ table.output_horz_separator
+ (column_zero_cash_surr_value
+ ,column_zero_irr_surr_value
+ ,*pos_y
+ ,output_mode
+ );
+ table.output_horz_separator
+ (column_nonzero_cash_surr_value
+ ,column_nonzero_irr_surr_value
+ ,*pos_y
+ ,output_mode
+ );
+ }
+};
+
+class reg_d_individual_guar_irr : public reg_d_individual_irr_base
+{
+ private:
+ base get_base() const override
+ {
+ return base::guaranteed;
+ }
+
+ std::string get_fixed_page_contents_template_name() const override
+ {
+ return "reg_d_indiv_guar_irr";
+ }
+
+ illustration_table_columns const& get_table_columns() const override
+ {
+ static illustration_table_columns const columns =
+ {{ "PolicyYear" , "Policy\nYear" ,
"999" }
+ ,{ "AttainedAge" , "End of\nYear Age" ,
"999" }
+ ,{ "GrossPmt" , "Premium\nOutlay" ,
"999,999" }
+ ,{ "CSVNet_GuaranteedZero" , "Cash Surr\nValue" ,
"999,999" }
+ ,{ "EOYDeathBft_GuaranteedZero" , "Death\nBenefit" ,
"9,999,999" }
+ ,{ "IrrCsv_GuaranteedZero" , "IRR on\nSurr Value" ,
"99.99%" }
+ ,{ "IrrDb_GuaranteedZero" , "IRR on\nDeath Bft" ,
"99.99%" }
+ ,{ "" , " " ,
"-" }
+ ,{ "CSVNet_Guaranteed" , "Cash Surr\nValue" ,
"999,999" }
+ ,{ "EOYDeathBft_Guaranteed" , "Death\nBenefit" ,
"9,999,999" }
+ ,{ "IrrCsv_Guaranteed" , "IRR on\nSurr Value" ,
"99.99%" }
+ ,{ "IrrDb_Guaranteed" , "IRR on\nDeath Bft" ,
"99.99%" }
+ };
+
+ return columns;
+ }
+};
+
+class reg_d_individual_curr_irr : public reg_d_individual_irr_base
+{
+ private:
+ base get_base() const override
+ {
+ return base::current;
+ }
+
+ std::string get_fixed_page_contents_template_name() const override
+ {
+ return "reg_d_indiv_curr_irr";
+ }
+
+ illustration_table_columns const& get_table_columns() const override
+ {
+ static illustration_table_columns const columns =
+ {{ "PolicyYear" , "Policy\nYear" ,
"999" }
+ ,{ "AttainedAge" , "End of\nYear Age" ,
"999" }
+ ,{ "GrossPmt" , "Premium\nOutlay" ,
"999,999" }
+ ,{ "CSVNet_CurrentZero" , "Cash Surr\nValue" ,
"999,999" }
+ ,{ "EOYDeathBft_CurrentZero" , "Death\nBenefit" ,
"9,999,999" }
+ ,{ "IrrCsv_CurrentZero" , "IRR on\nSurr Value" ,
"99.99%" }
+ ,{ "IrrDb_CurrentZero" , "IRR on\nDeath Bft" ,
"99.99%" }
+ ,{ "" , " " ,
"-" }
+ ,{ "CSVNet_Current" , "Cash Surr\nValue" ,
"999,999" }
+ ,{ "EOYDeathBft_Current" , "Death\nBenefit" ,
"9,999,999" }
+ ,{ "IrrCsv_Current" , "IRR on\nSurr Value" ,
"99.99%" }
+ ,{ "IrrDb_Current" , "IRR on\nDeath Bft" ,
"99.99%" }
+ };
+
+ return columns;
+ }
+};
+
+class reg_d_individual_curr : public page_with_tabular_report
+{
+ private:
+ enum
+ {column_policy_year
+ ,column_end_of_year_age
+ ,column_premium_outlay
+ ,column_premium_loads
+ ,column_admin_charges
+ ,column_curr_mortality_charges
+ ,column_curr_asset_charges
+ ,column_curr_investment_income
+ ,column_curr_account_value
+ ,column_curr_cash_surr_value
+ ,column_curr_death_benefit
+ ,column_max
+ };
+
+ std::string get_fixed_page_contents_template_name() const override
+ {
+ return "reg_d_indiv_curr";
+ }
+
+ illustration_table_columns const& get_table_columns() const override
+ {
+ static illustration_table_columns const columns =
+ {{ "PolicyYear" , "Policy\nYear" , "999" }
+ ,{ "AttainedAge" , "End of\nYear Age" , "999" }
+ ,{ "GrossPmt" , "Premium\nOutlay" , "999,999" }
+ ,{ "PremiumLoads" , "Premium\nLoads" , "999,999" }
+ ,{ "AdminCharges" , "Admin\nCharges" , "999,999" }
+ ,{ "COICharge_Current" , "Mortality\nCharges", "999,999" }
+ ,{ "SepAcctCharges_Current" , "Asset\nCharges" , "999,999" }
+ ,{ "GrossIntCredited_Current", "Investment\nIncome", "999,999" }
+ ,{ "AcctVal_Current" , "Account\nValue" , "999,999" }
+ ,{ "CSVNet_Current" , "Cash\nSurr Value" , "999,999" }
+ ,{ "EOYDeathBft_Current" , "Death\nBenefit" , "9,999,999" }
+ };
+
+ return columns;
+ }
+
+ bool should_show_column(Ledger const& ledger, int column) const override
+ {
+ // One column should be hidden for composite ledgers.
+ return column != column_end_of_year_age || !ledger.is_composite();
+ }
+
+ void render_or_measure_extra_headers
+ (illustration_table_generator& table
+ ,html_interpolator const& interpolate_html
+ ,int* pos_y
+ ,enum_output_mode output_mode
+ ) const override
+ {
+ table.output_super_header
+ (interpolate_html
+ ("{{InitAnnSepAcctGrossInt_Guaranteed}} Hypothetical Rate of
Return*"
+ ).as_html()
+ ,column_curr_investment_income
+ ,column_max
+ ,pos_y
+ ,output_mode
+ );
+
+ *pos_y += table.get_separator_line_height();
+ table.output_horz_separator
+ (column_curr_investment_income
+ ,column_max
+ ,*pos_y
+ ,output_mode
+ );
+ }
+};
+
+// Private individual placement illustration.
+class pdf_illustration_reg_d_individual : public pdf_illustration
+{
+ public:
+ pdf_illustration_reg_d_individual
+ (Ledger const& ledger
+ ,fs::path const& output
+ )
+ :pdf_illustration(ledger, output)
+ {
+ auto const& invar = ledger.GetLedgerInvariant();
+
+ // Define variables specific to this illustration.
+ add_abbreviated_variable("CorpName", 140);
+ add_abbreviated_variable("Insured1", 140);
+
+ // Add all the pages.
+ numbered_page::start_numbering();
+ add<standard_page>("reg_d_indiv_cover_page");
+ add<reg_d_individual_guar_irr>();
+ add<reg_d_individual_curr_irr>();
+ add<reg_d_individual_curr>();
+ add<standard_page>("reg_d_indiv_notes1");
+ add<standard_page>("reg_d_indiv_notes2");
+ add<standard_page>("reg_d_indiv_notes3");
+ if(invar.SupplementalReport)
+ {
+ add<standard_supplemental_report>
+ (get_interpolator()
+ ,"reg_d_indiv_supp_report"
+ );
+ }
+ }
+
+ std::string get_upper_footer_template_name() const override
+ {
+ return "reg_d_indiv_footer_upper";
+ }
+
+ std::string get_lower_footer_template_name() const override
+ {
+ return "reg_d_indiv_footer_lower";
+ }
+};
+
+class ledger_pdf_generator_wx : public ledger_pdf_generator
+{
+ public:
+ static std::shared_ptr<ledger_pdf_generator> do_create()
+ {
+ return std::make_shared<ledger_pdf_generator_wx>();
+ }
+
+ ledger_pdf_generator_wx() = default;
+ ledger_pdf_generator_wx(ledger_pdf_generator_wx const&) = delete;
+ ledger_pdf_generator_wx& operator=(ledger_pdf_generator_wx const&) =
delete;
+
+ void write(Ledger const& ledger, fs::path const& output) override;
+
+ private:
+};
+
+void ledger_pdf_generator_wx::write
+ (Ledger const& ledger
+ ,fs::path const& output
+ )
+{
+ std::unique_ptr<pdf_illustration> pdf_ill;
+
+ auto const z = ledger.ledger_type();
+ switch(z)
+ {
+ case mce_ill_reg:
+ pdf_ill = std::make_unique<pdf_illustration_regular>(ledger,
output);
+ break;
+ case mce_nasd:
+ pdf_ill = std::make_unique<pdf_illustration_nasd>(ledger, output);
+ break;
+ case mce_group_private_placement:
+ pdf_ill = std::make_unique<pdf_illustration_reg_d_group>(ledger,
output);
+ break;
+ case mce_individual_private_placement:
+ pdf_ill =
std::make_unique<pdf_illustration_reg_d_individual>(ledger, output);
+ break;
+ default:
+ alarum() << "Unknown ledger type '" << z << "'." << LMI_FLUSH;
+ }
+
+ pdf_ill->render_all();
+}
+
+volatile bool ensure_setup = ledger_pdf_generator::set_creator
+ (ledger_pdf_generator_wx::do_create
+ );
+
+} // Unnamed namespace.
diff --git a/output_mode.hpp b/output_mode.hpp
new file mode 100644
index 0000000..91c79e0
--- /dev/null
+++ b/output_mode.hpp
@@ -0,0 +1,35 @@
+// Output mode enum used in PDF generation helpers.
+//
+// Copyright (C) 2017 Gregory W. Chicares.
+//
+// This program is free software; you can redistribute it and/or modify
+// it under the terms of the GNU General Public License version 2 as
+// published by the Free Software Foundation.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program; if not, write to the Free Software Foundation,
+// Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+//
+// http://savannah.nongnu.org/projects/lmi
+// email: <address@hidden>
+// snail: Chicares, 186 Belle Woods Drive, Glastonbury CT 06033, USA
+
+#ifndef output_mode_hpp
+#define output_mode_hpp
+
+#include "config.hpp"
+
+/// Convenient enum used with functions that can either actually render
+/// something or just pretend doing it in order to compute the space that would
+/// be taken by it, in the layout phase.
+enum enum_output_mode
+ {e_output_normal
+ ,e_output_measure_only
+ };
+
+#endif // output_mode_hpp
diff --git a/pdf_writer_wx.cpp b/pdf_writer_wx.cpp
new file mode 100644
index 0000000..5ae36c2
--- /dev/null
+++ b/pdf_writer_wx.cpp
@@ -0,0 +1,249 @@
+// PDF generation helpers.
+//
+// Copyright (C) 2017 Gregory W. Chicares.
+//
+// This program is free software; you can redistribute it and/or modify
+// it under the terms of the GNU General Public License version 2 as
+// published by the Free Software Foundation.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program; if not, write to the Free Software Foundation,
+// Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+//
+// http://savannah.nongnu.org/projects/lmi
+// email: <address@hidden>
+// snail: Chicares, 186 Belle Woods Drive, Glastonbury CT 06033, USA
+
+#include "pchfile_wx.hpp"
+
+#include "pdf_writer_wx.hpp"
+
+#include "alert.hpp"
+#include "global_settings.hpp"
+#include "html.hpp"
+
+#include <wx/filesys.h>
+#include <wx/html/htmlcell.h>
+
+namespace
+{
+
+// These margins are arbitrary and can be changed to conform to subjective
+// preferences.
+constexpr int horz_margin = 24;
+constexpr int vert_margin = 36;
+
+wxPrintData make_print_data
+ (wxString const& output_filename
+ ,wxPrintOrientation orientation
+ )
+{
+ wxPrintData print_data;
+ print_data.SetPaperId(wxPAPER_LETTER);
+ print_data.SetFilename(output_filename);
+ print_data.SetOrientation(orientation);
+ return print_data;
+}
+
+} // Unnamed namespace.
+
+pdf_writer_wx::pdf_writer_wx
+ (wxString const& output_filename
+ ,wxPrintOrientation orientation
+ ,std::array<int, 7> const* html_font_sizes
+ )
+ :print_data_ {make_print_data(output_filename, orientation)}
+ ,pdf_dc_ {print_data_}
+ ,html_parser_ {nullptr}
+ ,total_page_size_ {pdf_dc_.GetSize()}
+{
+ // Ensure that the output is independent of the current display resolution:
+ // it seems that this is only the case with the PDF map mode and wxDC mode
+ // different from wxMM_TEXT.
+ pdf_dc_.SetMapModeStyle(wxPDF_MAPMODESTYLE_PDF);
+
+ // For simplicity, use points for everything: font sizers are expressed in
+ // them anyhow, so it's convenient to use them for everything else too.
+ pdf_dc_.SetMapMode(wxMM_POINTS);
+
+ pdf_dc_.StartDoc(wxString()); // Argument is not used.
+ pdf_dc_.StartPage();
+
+ // Use a standard PDF Helvetica font (without embedding any custom fonts in
+ // the generated file, the only other realistic choice is Times New Roman).
+ pdf_dc_.SetFont
+ (wxFontInfo
+ (html_font_sizes
+ ? html_font_sizes->at(2)
+ : 8
+ )
+ .Family(wxFONTFAMILY_SWISS)
+ .FaceName("Helvetica")
+ );
+
+ // Create an HTML parser to allow easily adding HTML contents to the
output.
+ html_parser_.SetDC(&pdf_dc_);
+ if(html_font_sizes)
+ {
+ html_parser_.SetFonts
+ ("Helvetica"
+ ,"Courier"
+ ,html_font_sizes->data()
+ );
+ }
+ else
+ {
+ html_parser_.SetStandardFonts
+ (pdf_dc_.GetFont().GetPointSize()
+ ,"Helvetica"
+ ,"Courier"
+ );
+ }
+
+ // Create the virtual file system object for loading images referenced from
+ // HTML and interpret relative paths from the data directory.
+ html_vfs_.reset(new wxFileSystem());
+ html_vfs_->ChangePathTo
+ (global_settings::instance().data_directory().string()
+ ,true /* argument is a directory, not file path */
+ );
+ html_parser_.SetFS(html_vfs_.get());
+}
+
+/// Output an image at the given scale into the PDF.
+///
+/// The scale specifies how many times the image should be shrunk:
+/// scale > 1 makes the image smaller, while scale < 1 makes it larger.
+///
+/// Updates pos_y by increasing it by the height of the specified
+/// image at the given scale.
+
+void pdf_writer_wx::output_image
+ (wxImage const& image
+ ,char const* image_name
+ ,double scale
+ ,int x
+ ,int* pos_y
+ ,enum_output_mode output_mode
+ )
+{
+ int const y = wxRound(image.GetHeight() / scale);
+
+ switch(output_mode)
+ {
+ case e_output_normal:
+ {
+ // Use wxPdfDocument API directly as wxDC doesn't provide a way to
+ // set the image scale at PDF level and also because passing via
+ // wxDC wastefully converts wxImage to wxBitmap only to convert it
+ // back to wxImage when embedding it into the PDF.
+ wxPdfDocument* const pdf_doc = pdf_dc_.GetPdfDocument();
+ LMI_ASSERT(pdf_doc);
+
+ pdf_doc->SetImageScale(scale);
+ pdf_doc->Image(image_name, image, x, *pos_y);
+ pdf_doc->SetImageScale(1);
+ }
+ break;
+ case e_output_measure_only:
+ // Do nothing.
+ break;
+ default:
+ {
+ alarum() << "Case " << output_mode << " not found." << LMI_FLUSH;
+ }
+ }
+
+ *pos_y += y;
+}
+
+/// Render, or just pretend rendering in order to measure it, the given HTML
+/// contents at the specified position wrapping it at the given width.
+/// Return the height of the output (using this width).
+
+int pdf_writer_wx::output_html
+ (int x
+ ,int y
+ ,int width
+ ,html::text const& html
+ ,enum_output_mode output_mode
+ )
+{
+ // We don't really want to change the font, but to preserve the current DC
+ // font which is changed by rendering the HTML contents.
+ wxDCFontChanger preserve_font(pdf_dc_, wxFont());
+
+ auto const html_str = wxString::FromUTF8(html.as_html());
+ std::unique_ptr<wxHtmlContainerCell> const cell
+ (static_cast<wxHtmlContainerCell*>(html_parser_.Parse(html_str))
+ );
+ LMI_ASSERT(cell);
+
+ cell->Layout(width);
+ switch(output_mode)
+ {
+ case e_output_normal:
+ {
+ wxHtmlRenderingInfo rendering_info;
+ cell->Draw
+ (pdf_dc_
+ ,x
+ ,y
+ ,0
+ ,std::numeric_limits<int>::max()
+ ,rendering_info
+ );
+ }
+ break;
+ case e_output_measure_only:
+ // Do nothing.
+ break;
+ default:
+ {
+ alarum() << "Case " << output_mode << " not found." << LMI_FLUSH;
+ }
+ }
+
+ return cell->GetHeight();
+}
+
+int pdf_writer_wx::get_horz_margin() const
+{
+ return horz_margin;
+}
+
+int pdf_writer_wx::get_vert_margin() const
+{
+ return vert_margin;
+}
+
+int pdf_writer_wx::get_page_width() const
+{
+ return total_page_size_.x - 2 * horz_margin;
+}
+
+int pdf_writer_wx::get_total_width() const
+{
+ return total_page_size_.x;
+}
+
+int pdf_writer_wx::get_page_height() const
+{
+ return total_page_size_.y - 2 * vert_margin;
+}
+
+int pdf_writer_wx::get_page_bottom() const
+{
+ return total_page_size_.y - vert_margin;
+}
+
+pdf_writer_wx::~pdf_writer_wx()
+{
+ // This will finally generate the PDF file.
+ pdf_dc_.EndDoc();
+}
diff --git a/pdf_writer_wx.hpp b/pdf_writer_wx.hpp
new file mode 100644
index 0000000..4634f7c
--- /dev/null
+++ b/pdf_writer_wx.hpp
@@ -0,0 +1,100 @@
+// PDF generation helpers.
+//
+// Copyright (C) 2017 Gregory W. Chicares.
+//
+// This program is free software; you can redistribute it and/or modify
+// it under the terms of the GNU General Public License version 2 as
+// published by the Free Software Foundation.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program; if not, write to the Free Software Foundation,
+// Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+//
+// http://savannah.nongnu.org/projects/lmi
+// email: <address@hidden>
+// snail: Chicares, 186 Belle Woods Drive, Glastonbury CT 06033, USA
+
+#ifndef pdf_writer_wx_hpp
+#define pdf_writer_wx_hpp
+
+#include "config.hpp"
+
+#include "assert_lmi.hpp"
+#include "output_mode.hpp"
+
+#include <wx/html/winpars.h>
+
+#include <wx/pdfdc.h>
+
+#include <array>
+#include <memory> // std::unique_ptr
+
+class wxFileSystem;
+
+namespace html { class text; }
+
+class pdf_writer_wx
+{
+ public:
+ // Optional html_font_sizes array allows to override default font sizes for
+ // the standard HTML3 fonts (1..7).
+ pdf_writer_wx
+ (wxString const& output_filename
+ ,wxPrintOrientation orientation
+ ,std::array<int, 7> const* html_font_sizes = nullptr
+ );
+
+ pdf_writer_wx(pdf_writer_wx const&) = delete;
+ pdf_writer_wx& operator=(pdf_writer_wx const&) = delete;
+
+ ~pdf_writer_wx();
+
+ // High level functions which should be preferably used if possible.
+ int output_html
+ (int x
+ ,int y
+ ,int width
+ ,html::text const& html
+ ,enum_output_mode output_mode = e_output_normal
+ );
+
+ void output_image
+ (wxImage const& image
+ ,char const* image_name
+ ,double scale
+ ,int x
+ ,int* pos_y
+ ,enum_output_mode output_mode = e_output_normal
+ );
+
+ // Accessors allowing to use lower level wxDC API directly.
+ wxDC& dc() { return pdf_dc_; }
+
+ // Page metrics: the page width and height are the size of the page region
+ // reserved for the normal contents, excluding horizontal and vertical
+ // margins. Total width and height include the margins.
+ int get_horz_margin() const;
+ int get_vert_margin() const;
+ int get_page_width() const;
+ int get_total_width() const;
+ int get_page_height() const;
+ int get_page_bottom() const;
+
+ private:
+ wxPrintData print_data_;
+ wxPdfDC pdf_dc_;
+
+ // Order is potentially important here: html_parser_ uses html_vfs_, so
+ // must be declared after it in order to be destroyed before it.
+ std::unique_ptr<wxFileSystem> html_vfs_;
+ wxHtmlWinParser html_parser_;
+
+ wxSize const total_page_size_;
+};
+
+#endif // pdf_writer_wx_hpp