Psalms and Canticles

 The class for representing a psalm is at the opposite end of the spectrum from the PsalmSpec class.  It's a straightforward concrete class, with no interfaces or inheritance involved. (This has the drawback that one can't make it immutable, or provide a partial interface for it which is used as an immutable interface; I can live with that, given that this is very deep down in the library and wouldn't be a class that would be published if modules were in use.)

A psalm isn't quite as simple as a set of verses.  In Anglican use, both psalms and subsections of psalms are associated with a tag in Latin which is the first two or three words of the psalm. (E.g. Psalm 23 has Dominus regit me as a tag.) Most psalms have only one (implicit) section but some psalms have two or more subsections.

Let's look at a section first:

  class Section

  {

  public:

    class IStreamer

    {

    public:

      virtual ~IStreamer();


      virtual void stream(const uint16_t inNumber,

                          std::string_view inVerse) const = 0;

    };


    class FormattedStreamer : public IStreamer

    {


    public:

      explicit FormattedStreamer(

          const IEncapsulatedOfficeFormatter &inFormatter)

          : m_formatter(inFormatter)

      {

      }


      ~FormattedStreamer() override;


      void stream(const uint16_t inNumber,

                  std::string_view inVerse) const override;

    private:

      const IEncapsulatedOfficeFormatter &m_formatter;

    };


    explicit Section(std::string_view inTag) : m_tag(inTag) {}


    bool addVerse(uint16_t inNumber, const std::string_view inVerse);


    constexpr uint16_t

    getLowest() const

    {

      return m_lowest;

    }

    constexpr uint16_t

    getHighest() const

    {

      return m_highest;

    }


    auto

    getLength() const ->decltype(std::ssize(std::map<uint16_t, std::string>()))

    {

      return std::ssize(m_verses);

    }

    constexpr const std::string &

    getTag() const

    {

      return m_tag;

    }


    void formatVerse(const IEncapsulatedOfficeFormatter &inFormatter,

                const uint16_t inIndex) const

    {

      getOneVerse(FormattedStreamer(inFormatter), inIndex);

    }


    bool

    inRange(const uint16_t inVal) const

    {

      return ((inVal >= m_lowest) && (inVal <= m_highest));

    }

    void getAllVerses(const IStreamer &inStreamer) const;

  private:

    void getOneVerse(const IStreamer &inStreamer,

                     const uint16_t inIndex) const;

    std::map<uint16_t, std::string> m_verses;

    std::string m_tag;

    uint16_t m_lowest = 0;

    uint16_t m_highest = 0;

  };

Originally more streamer types were made available; they were reduced because the flexibility of the formatter interface rendered them unnecessary and redundant.  The capability of extending the IStreamer class was left in place to support future flexibility.

Note that a section requires that verses be numbered.  Adding a verse checks the value of the number:

bool Psalm::Section::addVerse(uint16_t inNumber, const std::string_view inVerse)

{

  if (m_highest < inNumber)

    m_highest = inNumber;

  if ((m_lowest == 0) || (m_lowest > inNumber))

    m_lowest = inNumber;

  if (m_verses.contains(inNumber))

    return false;

  m_verses.emplace(inNumber, inVerse);

  return true;

}

This (and the additional checks made at the psalm level) are because the psalm verses are parsed in from an external XML document where the verses are numbered and the additional levels of validation were felt to be useful.

Language features: trailing return value formats using decltype; a few constexpr functions (map won't support constexpr, which limits its use); emplace for adding the verses; using ssize() for size.

The Streamer functions, essentially using the Streamer as a quasi-visitor, are:

void Psalm::Section::getOneVerse(const IStreamer &inStreamer,

                            const uint16_t inIndex) const

{

  auto verse = m_verses.find(inIndex);

  if (verse != m_verses.end())

    {

      inStreamer.stream(inIndex, verse->second);

    }

}

void Psalm::Section::getAllVerses(const IStreamer &inStreamer) const

{

  std::ranges::for_each(m_verses, [&](const auto &verse) {

    inStreamer.stream(verse.first, verse.second);

  });

}

which uses the C++20 ranges version of for_each.  Also note that although getOneVerse() does have an explicit iterator -- they're hard to avoid with find() -- the text is rendered somewhat cleaner by the use of auto for the return type of find().

The Psalm class itself (for which Section is an inner class) is much shorter:

class Psalm

{

  // Section code skipped

  public:

  constexpr Psalm() {}

  Psalm(const uint16_t inNumber, std::string_view inTag) : m_number(inNumber)

  {

    if (!inTag.empty())

      addSection(inTag);

  }

  constexpr uint16_t getNumber() const

  {

    return m_number;

  }


  auto getLength() const -> decltype(Section("").getLength());

  auto getSectionCount() const -> decltype(std::ssize(std::vector<Section>()))

  {

    return std::ssize(m_sections);

  }


  const std::string &getTag() const;

  bool addVerse(const uint16_t inNumber, const std::string &inVerse,

                const Log::ILogger &inLogger);

  void addSection(std::string_view inTag);

  void formatVerse(const IEncapsulatedOfficeFormatter &inFormatter,

                   const uint16_t inIndex) const;


  void formatSection(const IEncapsulatedOfficeFormatter &inFormatter,

                     const uint16_t inIndex) const;


private:

  std::vector<Section> m_sections;

  uint16_t m_number = 0;

};

There's more use of decltype for return values here.  The definition of length() uses accumulate() which passes the return type along:

auto Psalm::getLength() const-> decltype(Section("").getLength())

{

  decltype(Section("").getLength()) init(0);

  return std::accumulate(m_sections.begin(), m_sections.end(), init,

                         [](const decltype(init) inTally, const auto &elt) {

                           return inTally + elt.getLength();

                         });

}

Note that std::accumulate does not have a range version; that will be fold_left (C++23), not yet supported in my compiler.

Formatting sections and individual verses is fairly straightforward.

void Psalm::formatVerse(const IEncapsulatedOfficeFormatter &inFormatter,

                   const uint16_t inIndex) const

{

  if (!m_sections.empty()

      && ((inIndex > 0) && (inIndex <= m_sections.back().getHighest())))

    {

      std::ranges::for_each(m_sections

                                | std::views::filter([&inIndex](const auto s) {

                                    return s.inRange(inIndex);

                                  }),

                            [&](const auto &inSection) {

                              inSection.formatVerse(inFormatter, inIndex);

                            });

    }

}

The use of std::views::filter() here short-circuits what would otherwise require breaking up the call into separate calls for finding the section and then formatting the verse from it.  In a long set of sections it would be inefficient -- the logic filters every section, not finishing when the verse has been found -- but in practice this function is called only with six verses from Psalm 31 (seen already in the Psalm Spec), which has only one section.  (It would be faster to pass out the section and then allow the calling context to format the six verses, but that means exposing the Psalm internals, which we don't want to do.  So we provide general code and live with the small extra overhead.)

Most formatting is a whole section at a time, which is much more efficient:

void Psalm::formatSection(const IEncapsulatedOfficeFormatter &inFormatter,

                     const uint16_t inIndex) const

{

  if (!m_sections.empty() && ((inIndex > 0) && (inIndex <= m_sections.size())))

    {

      inFormatter.formatHeading(m_sections[inIndex - 1].getTag(), true);

      m_sections[inIndex - 1].getAllVerses(

          Section::FormattedStreamer(inFormatter));

    }

}

The longest and ugliest function is addVerse():

bool Psalm::addVerse(const uint16_t inNumber, const std::string &inVerse,

                     const Log::ILogger &inLogger)

{

  if (m_sections.empty())

    {

      if (inNumber == 1)

        m_sections.emplace_back(""s);

      else

{

  std::ostringstream str;

          str << "Psalm::addVerse(): attempting to add verse with number greater than 1 to empty psalm: "

              << inNumber;

          inLogger.log(Log::Severity::Error, str.str());

  return false;

}

    }

  else if ((m_sections.size() > 1) && (m_sections.back().getLength() == 0))

    {

      auto x = m_sections.crbegin();

      ++x;


      if (x->getHighest() != (inNumber - 1))

        {

          std::ostringstream str;

          str << "Psalm::addVerse(): Rejecting section because number in next to last "

                 "section does not match: "

              << x->getHighest() << "-" << (inNumber - 1);

          inLogger.log(Log::Severity::Error, str.str());

          return false;

        }

    }

  else if (m_sections.back().getHighest() != (inNumber - 1))

    {

      std::ostringstream str;

      str << "Psalm::addVerse(): Rejecting section because number in second or higher "

             "section does not match: "

          << m_sections.back().getHighest() << "-" << (inNumber - 1);

      inLogger.log(Log::Severity::Error, str.str());

      return false;

    }

  bool rval = m_sections.back().addVerse(inNumber, inVerse);

  if (!rval)

    {

      std::ostringstream str;

      str << "Psalm::addVerse(): Verse " << inNumber << " already exists in psalm";

      inLogger.log(Log::Severity::Error, str.str());

    }

  return rval;

}

Most of this isn't even error checking; it's error reporting.  Again, we want the error checking to assist with the parsing in of external data.  The error messages vary enough that trying to find common logic to pull out to simplify the error reporting is not practical.

Canticles

The office of Lauds uses a canticle from the Old Testament in place of one of the psalms (the Gospel Canticles are handled specially).  These are represented by a separate, and simpler, class.

Canticles have no subsections; they do not, by convention, have numbered verses.  The Lauda canticles are limited in number (seven as opposed to 150).  These allow us to simplify the class very considerably over the Psalm class.

1) The canticle texts are set in the constructors with in-memory initialization.  This allows the class itself to be made immutable -- no adding of verses in the interface.

2) We have only one formatting call, to print the entire canticle.

However, the interface does use a couple of enums which are used very heavily throughout the library. This is our first encounter with them.

enum class Days

{

  SUNDAY = 1,

  MONDAY = 2,

  TUESDAY = 3,

  WEDNESDAY = 4,

  THURSDAY = 5,

  FRIDAY = 6,

  SATURDAY = 7

};

The hours vary each day of the week in many ways.  This enum is passed around to represent the day.  (Occasionally this is not the literal day of the week -- some feats have "Psalms of Sunday", for example.)  There are functions for converting to and from strings and extracting a Days value from a  std::chrono::year_month_day time point.

The office structures are really very radically different during the last three days of Holy Week through Easter Week.  We don't even try to assimilate these to normal patterns, but pass around the values as special enum values:

enum class SpecialDays

{

  NONE,

  MAUNDY_THURSDAY,

  GOOD_FRIDAY,

  HOLY_SATURDAY,

  EASTER_WEEK

};

As with all of the enums in this project these use the C++11 class enum.

Here is the Canticle class:

class Canticle

{

public:

  Canticle() {}

  Canticle(const std::string &inName, const std::string &inTag,

           std::span<std::string> inVerses, const bool inGenerateGP = true)

      : m_name(inName), m_tag(inTag),

        m_verses(inVerses.begin(), inVerses.end()), m_generateGP(inGenerateGP)

  {}

  explicit Canticle(const Days inDay) { SetFieldsByDay(inDay); }

  Canticle(const SpecialDays inSD, const Days inDays);

  const std::string & getName() const

  {

    return m_name;

  }

  const std::string & getTag() const

  {

    return m_tag;

  }

  void formattedPrint(const IEncapsulatedOfficeFormatter &inFormatter) const;

private:

  std::string m_name;

  std::string m_tag;

  std::vector<std::string> m_verses;

  bool m_generateGP = true;

  void SetFieldsByDay(const Days inDay);

};

Like a psalm, a Canticle has an associated Latin tag.  The general constructor is used for handling the Athanasian Creed in Prime and the New Testament canticles in Vespers, Lauds, and Compline.

For the Lauds canticles, there's a usual constructor:

void

Canticle::SetFieldsByDay(const Days inDay)

{

  m_generateGP = (inDay != Days::SUNDAY);

  switch (inDay)

    {

      using enum Days;

    case SUNDAY:

      m_name = "Song of the Three Children";

      m_tag = "Benedicite, Omnia Opera";

      break;

    case MONDAY:

      m_name = "Song of Isaiah";

      m_tag = "Confitebor tibi";

      break;

    case TUESDAY:

      m_name = "Song of Hezekiah";

      m_tag = "Ego dixi";

      break;

    case WEDNESDAY:

      m_name = "Song of Hannah";

      m_tag = "Exultavit cor meum";

      break;

    case THURSDAY:

      m_name = "Song of Moses";

      m_tag = "Cantemus Domino";

      break;

    case FRIDAY:

      m_name = "Song of Habakkuk";

      m_tag = "Domine audivi";

      break;

    case SATURDAY:

      m_name = "Song of Moses";

      m_tag = "Audite caeli";

      break;

    default:

      m_name = "Unknown canticle";

      m_tag = "Nullum";

    }

  InitializeCanticleByDay(m_verses, inDay);

}

InitializeCanticleByDay() is a helper function defined in the anonymous namespace which actually sets the text of the canticle.  It's not shown here because of its considerable length.  The default value is allowed because of the theoretical possibility of casting an int to an unexpected value within the range of the underlying enum representation; I decided to assign a null value rather than throw, as this should in practice never happen.

Canticle::Canticle(const SpecialDays inSD, const Days inDays)

{

  if (inSD == SpecialDays::MAUNDY_THURSDAY)

    {

      m_name = "Song of Moses";

      m_tag = "Cantemus Domino";

      InitializeCanticleByDay(m_verses, Days::THURSDAY);

      m_generateGP = false;

    }

  else if (inSD == SpecialDays::GOOD_FRIDAY)

    {

      m_name = "Song of Habakkuk";

      m_tag = "Domine audivi";

      InitializeCanticleByDay(m_verses, Days::FRIDAY);

      m_generateGP = false;

    }

  else if (inSD == SpecialDays::HOLY_SATURDAY)

    {

      m_name = "Song of Hezekiah";

      m_tag = "Ego dixi";

      InitializeCanticleByDay(m_verses, Days::TUESDAY);

      m_generateGP = false;

    }

  else

    SetFieldsByDay(inDays);

}

Initializing for special days is shorter.  Note that the Canticle for Holy Saturday is that normally used on Tuesday...

Comments

Popular posts from this blog

Boundaries

LT Project: Author

State Machines