Hymns

 Hymns are, basically, just text.  Lineation is provided by newlines in the source from which the text is drawn, and indentation can be provided on a line-by-line basis but (e.g.) stanzaic structure doesn't get reflected in separate classes. So almost all the logic is about storing, retrieving, and referencing them.

Hymns have names, which are really Latin incipits. For example, the hymn for vespers in Advent has the "name" Conditor alme siderum.  This use is fairly common and convenient.

In the Sarum Rite, one of the Vespers hymns for apostles is special: the Annue, Christe gets a special verse interpolated and some pronouns are changed to plural where the commemoration is of two saints, e.g. Simon and Jude, so there's a special interface just for that one special case.

class IHymnSource

{

public:

  virtual ~IHymnSource();


  virtual std::string getHymn(const std::string &inName,

                              const HymnSeasons inSeason) const = 0;


  virtual std::string getAnnueChriste(const std::string &inInterpolatedVerse,

                                      const bool inIsPlural) const = 0;

};

The interface defines an immutable object.  There's also an interface to use when building a hymn collection, IHymnSet:

class IHymnSet

{

public:

  virtual ~IHymnSet();


  virtual void addHymn(const std::string &inName, const std::string &inHymn,

                       const bool inUseVariableDoxology) = 0;

};

The simplest implementation of IHymnSource is in the ComplineChapter.  Compline hymns are their own little world and they don't reference the Annue, Christe, so the hymn values are hardcoded into the ComplineChapter class (already seen in the post on the Chapter classes generally).

The general implementation works about how you might expect: hymns are stored in a map.

class HymnSource : public IHymnSource, public IHymnSet

{

  struct Hymn

  {

    std::string body;

    bool usesVariableDoxology = true;

  };


public:

  ~HymnSource() override;


  std::string getHymn(const std::string &inName,

                      const HymnSeasons inSeason) const override;


  void addHymn(const std::string &inName, const std::string &inHymn,

               const bool inUseVariableDoxology) override;


  std::string getAnnueChriste(const std::string &inInterpolatedVerse,

      const bool inIsPlural) const override;

  

private:

  std::map<std::string, Hymn> m_hymns;

};

Some hymns have doxologies in a standard metrical format that can change by the season. There's a separate class called Doxology to handle that; an instance is declared in the anonymous namespace and then referenced from the getHymn() method. (The Doxology class itself is a fixed map of stanzas keyed by the HymnSeason enum, and not really worth displaying in detail.)


std::string HymnSource::getHymn(const std::string &inName,

                    const HymnSeasons inSeason) const

{

  auto iter = m_hymns.find(inName);

  if (iter == m_hymns.end())

    return {};

  if (iter->second.usesVariableDoxology)

    {

      return iter->second.body + dox.getVerses(inSeason);

    }

  else

    return iter->second.body;

}


addHymn() is of interest only in that it uses the recently added insert_or_assign method for std::map.

void HymnSource::addHymn(const std::string &inName, const std::string &inHymn,

                    const bool inUseVariableDoxology)

{

  m_hymns.insert_or_assign(inName, Hymn{ inHymn, inUseVariableDoxology });

}

Parsing hymns

With some special exceptions, like the Compline hymns or the Annue, Christe, hymns are stored in an XML file, witha very simple format indeed.

<hymns>

  <hymn name="Aeterne Rex altissime">

    Eternal Monarch, King most high,

    Whose blood hath brought redemption nigh,

    By whom the death of death was wrought,

    And conqu'ring grace's battle fought:


    Ascending to the Throne of might,

    And seated at the Father's right,

    All power in heaven is Jesu's own,

    That here His Manhood had not known.


    That so,in Nature's triple frame,

    Each heavenly and each earthly name,

    And things in hell's abyss abhorred,

    May bend the knee and own Him Lord.


    Yea, angels tremble when they see

    How changed is our humanity.

    That flesh hath purged what flesh had stained,

    And God, the flesh of God, hath reigned.


    Be Thou our Joy and Thou our Guard,

    Who art to be our great Reward :

    Our glory and our boast in Thee,

    For ever and for ever be!


    All glory, Lord, to Thee we pay,

    Ascending o'er the stars to-day;

    All glory, as is ever meet,

    To Father and to Paraclete. Amen.

  </hymn>

  <hymn name="O Lux Beata Trinitas" doxology="NORMAL">

    O Trinity of blessèd light,

    O Unity of princely might,

    The fiery sun now goes his way;

    Shed Thou within our hearts a ray.


    To Thee our morning song of praise,

    To Thee our evening prayer we raise;

    O grant us with Thy saints on high

    To praise Thee through eternity.

  </hymn>

...

</hymns>


The doxology tag determines whether the hymn takes a variable doxology (NORMAL) or not.  It's also used to avoid storing, repetitively, the standard Easter doxology of two verses.

The HymnParser is another tokenizing parse like that for the psalms, but even simpler; there are fewer states and fewer types of tokens.

class HymnParser

{

  enum class States

  {

    Beginning,

    InHymns,

    InToken,

    ReadingHymn,

    End

  };


  enum class TokenTypes

  {

    Name,

    DoxologyType,

    Text,

    End,

    Error

  };


public:

  HymnParser(const Log::ILogger &inLogger, std::istream &inStream,

             IHymnSet &inSink)

      : m_logger(inLogger), m_stream(inStream), m_sink(inSink),

        m_state(States::Beginning)

  { }


  void

  parse()

  {

    while (m_state != States::End)

      {

        if (!parseOne())

          break;

      }

  }


private:

  bool parseOne();

  std::pair<std::string, TokenTypes> getNextToken();

  const Log::ILogger &m_logger;

  std::istream &m_stream;

  IHymnSet &m_sink;

  std::string m_currentName;

  std::string m_currentDoxology;

  std::string m_currentToken;

  States m_state;

};


Tokenizing, in the getNextToken() function is very much the most complex function in the class:

std::pair<std::string, HymnParser::TokenTypes>

HymnParser::getNextToken()

{

  if (m_state == States::End)

    return std::make_pair(""s, TokenTypes::End);

  if (m_state == States::InToken)

    {

      if (m_currentToken.empty())

        {

          m_currentToken.clear();

          m_state = States::ReadingHymn;

        }

      else

        {

          auto offset = m_currentToken.find("doxology=\"");

          if (offset == std::string::npos)

            {

              m_currentToken.clear();

              m_state = States::ReadingHymn;

            }

          else

            {

              auto rest = m_currentToken.substr(offset + 10);

              offset = rest.find('"');

              if (offset == std::string::npos)

                {

                  m_state = States::End;

                  return std::make_pair(m_currentToken, TokenTypes::Error);

                }

              auto rval = rest.substr(0, offset);

              m_currentToken.clear();

              m_state = States::ReadingHymn;

              return std::make_pair(rval, TokenTypes::DoxologyType);

            }

        }

    }


  std::string s;

  while (m_state != States::End)

    {

      std::getline(m_stream, s);

      boost::algorithm::trim(s);

      switch (m_state)

        {

          using enum States;

        case Beginning:

          {

            if (!m_stream.good())

              {

                m_state = States::End;

                return std::make_pair(""s, TokenTypes::End);

              }

            // Ignore anything before opening  tag

            if (s == "<hymns>"s)

              m_state = States::InHymns;

          }

          break;

        case InHymns:

          {

            if (!m_stream.good())

              {

                m_state = States::End;

                return std::make_pair(""s, TokenTypes::Error);

              }

            if (!s.empty())

              {

if (s == "</hymns>")

  {

    return std::make_pair(""s, TokenTypes::End);

  }

                const static std::string TagBegin("<hymn name=\"");

                if (s.length() < TagBegin.length())

                  {

    if (!s.starts_with("<--"))

      {

m_state = States::End;

return std::make_pair(s, TokenTypes::Error);

      }

                  }

                if (s.starts_with(TagBegin))

                  {

                    auto rest = s.substr(TagBegin.length());

                    auto offset = rest.find('"');

                    if (offset == std::string::npos)

                      return std::make_pair(s, TokenTypes::Error);

                    auto name = rest.substr(0, offset);

                    m_currentToken = rest.substr(offset + 1);

                    m_state = States::InToken;

                    return std::make_pair(name, TokenTypes::Name);

                  }

                else

                  {

    if (s[0] == '<')

      {

if (!s.starts_with("<--"))

  return std::make_pair("Unrecognized tag: "s + s, TokenTypes::Error);

      }

    else

      {

m_state = States::End;

return std::make_pair(s, TokenTypes::Error);

      }

                  }

              }

          }

          break;

        case InToken:

          // Internal parser error...

          m_state = States::End;

          return std::make_pair(s, TokenTypes::Error);

          break;

        case ReadingHymn:

          if (!m_stream.good())

            {

              m_state = States::End;

              return std::make_pair(""s, TokenTypes::Error);

            }

          if (s == "</hymn>"s)

            {

              m_state = States::InHymns;

              return std::make_pair(m_currentToken, TokenTypes::Text);

            }

          else if (s.starts_with("<hymn "s))

            {

              m_state = States::End;

              return std::make_pair("Unterminated hymn"s, TokenTypes::Error);

            }

          else if (s.empty())

            m_currentToken.append("\n");

          else

            {

              m_currentToken.append(s).append("\n");

            }

          break;

        default:

          return std::make_pair(""s, TokenTypes::End);

        }

    }

  m_state = States::End;

  return std::make_pair(""s, TokenTypes::End);

}

By comparison, the semantic level making use of the tokens is much simpler:

bool HymnParser::parseOne()

{

  if (!m_stream.good())

    {

      m_state = States::End;

      return false;

    }

  switch (auto [s, t] = getNextToken(); t)

    {

      using enum TokenTypes;

    case Name:

      m_currentName = s;

      m_currentDoxology.clear();

      return true;

    case DoxologyType:

      m_currentDoxology = s;

      return true;

    case Text:

      if (m_currentDoxology == "EASTER"s)

        {

  static const std::string EasterDoxology = "\n" + Doxologies().getVerses(HymnSeasons::EASTER);

          s.append(EasterDoxology);

        }

      m_sink.addHymn(m_currentName, s, (m_currentDoxology == "NORMAL"s));

      return true;

    case Error:

      m_logger.log(Log::Severity::Error, "Error in parsing hymns: " + s);

      [[fallthrough]];

    case End:

      [[fallthrough]];

    default:

      return false;

    }

}


Comments

Popular posts from this blog

Boundaries

State Machines

Considerations on an Optimization