The Liturgical Calendar

The office is structured around the liturgical calendar, which is divided into two parts: the proper of time (Sundays and major feasts attached to either the Easter Cycle or the Christmas Cycle) and the proper of saints (misnamed because it includes all the days which are determined by the day of the year, regardless of their title).  These require separate handling (though there are places where the two interact).

Proper of Time

The core interface for dealing with the proper of seasons is IDateCalculator.  This provides the information based on the Easter and Christmas cycles, with some other calculated values: octaves, which are usually but not always attached to feasts in this grouping, and some moveable dates based on the Easter Cycle. (Lady Day is normally March 25 but can be shifted into April as a result of Holy Week or Easter Week, which take precedence.)

class IDateCalculator

{

public:

  virtual ~IDateCalculator();

  virtual Feast getOctaveInfo(const unsigned int inDay,

                              const unsigned int inMonth) const = 0;

  virtual HymnSeasons getSeason(const unsigned int inDay,

                                const unsigned int inMonth) const = 0;

  virtual bool isInEasterPrivilege(const unsigned int inDay,

                                   const unsigned int inMonth) const = 0;

  virtual bool isSunday(const unsigned int inDay,

                        const unsigned int inMonth) const = 0;

  virtual std::string getSundayName(const unsigned int inDay,

                                    const unsigned int inMonth) const = 0;

  virtual std::pair<std::string, std::string>

  getFerialName(const unsigned int inDay,

const unsigned int inMonth) const = 0;

  virtual const KalendarKey &getLadyDay() const = 0;


  virtual bool isCorpusChristi(const unsigned int inDay,

       const unsigned int inMonth) const = 0;

  virtual bool isSacredHeart(const unsigned int inDay,

     const unsigned int inMonth) const = 0;

  virtual int getYear() const = 0;

};

The KalendarKey struct is used for the proper of saints, but shows up here as a return value:

struct KalendarKey

{

  unsigned int day;

  unsigned int month;

  int operator<=>(const BreviaryLib::KalendarKey &inSecond) const

  {

    int diff = static_cast<int>(month) - static_cast<int>(inSecond.month);

    if (diff != 0)

      return diff;

    return static_cast<int>(day) - static_cast<int>(inSecond.day);

  }

};

(The spelling Kalendar is traditional for the ecclesiastical calendar.)

There is one principal implementation (the interface has a different implementation for unit tests).

class DateCalculator : public IDateCalculator

{

public:

  explicit DateCalculator(const std::string &inEasterSunday);

  // Uses this year

  DateCalculator();

  ~DateCalculator() override;

  Feast getOctaveInfo(const unsigned int inDay,

                      const unsigned int inMonth) const override;

  HymnSeasons getSeason(const unsigned int inDay,

                        const unsigned int inMonth) const override;

  bool isInEasterPrivilege(const unsigned int inDay,

                           const unsigned int inMonth) const override

  {

    KalendarKey k{ inDay, inMonth };

    return ((k >= m_beginHolyWeek) && (k <= m_endEasterWeek));

  }

  bool isSunday(const unsigned int inDay,

                const unsigned int inMonth) const override

  {

    return m_sundays.isSunday(inDay, inMonth);

  }

  std::string getSundayName(const unsigned int inDay,

                            const unsigned int inMonth) const override

  {

    return m_sundays.getSundayName(inDay, inMonth);

  }

  std::pair<std::string, std::string>

  getFerialName(const unsigned int inDay,

                const unsigned int inMonth) const override

  {

    return m_sundays.getFerialName(inDay, inMonth, m_year);

  }

  const KalendarKey &getLadyDay() const override { return LadyDay; }

  bool isCorpusChristi(const unsigned int inDay,

       const unsigned int inMonth) const override

  {

    return std::chrono::year_month_day{ m_year, std::chrono::month{ inMonth },

      std::chrono::day{ inDay } } == CorpusChristi;

  }

  bool isSacredHeart(const unsigned int inDay,

     const unsigned int inMonth) const override

  {

    return std::chrono::year_month_day{ m_year, std::chrono::month{ inMonth },

      std::chrono::day{ inDay } } == SacredHeart;

  }

  int getYear() const override { return static_cast<int>(m_year); }

private:

  std::chrono::year m_year;

  KalendarKey m_beginHolyWeek;

  KalendarKey m_endEasterWeek;

  SundayIndex m_sundays;

  std::chrono::year_month_day Septuagesima;

  KalendarKey LadyDay;

  std::chrono::year_month_day Easter;

  AdventSunday m_AdventSunday;

  std::chrono::year_month_day CorpusChristi;

  std::chrono::year_month_day SacredHeart;

  OctaveSet m_octaves;

  void completeInitialization();

};

Much of the logic is embedded in the held classes: we'll get to them after looking at the high-level functions.

There are two constructors.  The first sets up the object based on "today" as a default value:

DateCalculator::DateCalculator()

{

  const std::chrono::time_point now{ std::chrono::system_clock::now() };

  const std::chrono::year_month_day ymd{ std::chrono::floor<std::chrono::days>(now) };

  m_year = ymd.year();

  const static EasterTable table;

  Easter = table.getEasterDate(static_cast<int>(m_year));

  m_sundays.add(Easter, "Easter"s);

  completeInitialization();

}

EasterTable is a class allowing lookup of Easter dates from the years from 2023 to 2040. (No, I'm not going to do Easter calculations).

/**

   Data taken from the BCP Table of Moveable Feasts.

 **/

class EasterTable {

public:

  EasterTable();

  

  const std::string& getEaster(const int inYear) const;


  const std::chrono::year_month_day getEasterDate(const int inYear) const;

private:

  std::map<int, std::string> m_data;

};

EasterTable::EasterTable()

    : m_data{ { 2023, "2023-04-09"s }, { 2024, "2024-03-31"s },

              { 2025, "2025-04-20"s }, { 2026, "2026-04-05"s },

              { 2027, "2027-03-28"s }, { 2028, "2028-04-16"s },

              { 2029, "2029-04-01"s }, { 2030, "2030-04-21"s },

              { 2031, "2031-04-13"s }, { 2032, "2032-03-28"s },

              { 2033, "2033-04-17"s }, { 2034, "2034-04-09"s },

              { 2035, "2035-03-25"s }, { 2036, "2036-04-13"s },

              { 2037, "2037-04-05"s }, { 2038, "2038-04-25"s },

              { 2039, "2039-04-10"s }, { 2040, "2040-04-01"s } }

}

const std::string &

EasterTable::getEaster(const int inYear) const

{

  auto iter = m_data.find(inYear);

  if (iter == m_data.end())

    {

      throw KalendarException("EasterTable::getEaster()", "Date provided "s

      + boost::lexical_cast<std::string>(inYear)

      + " not supported for Easter lookup"s);

    }

  return iter->second;

}

const std::chrono::year_month_day EasterTable::getEasterDate(const int inYear) const

{

  auto iter = m_data.find(inYear);

  if (iter == m_data.end())

    {

      throw KalendarException("EasterTable::getEasterDate()", "Date provided "s

      + boost::lexical_cast<std::string>(inYear)

      + " not supported for Easter lookup"s);

    }

  auto d = iter->second;

  return std::chrono::year_month_day(std::chrono::year(std::atoi(d.substr(0,4).c_str())),

     std::chrono::month(std::atoi(d.substr(5,2).c_str())),

     std::chrono::day(std::atoi(d.substr(8).c_str())));

}

The second constructor takes a provided date of Easter in an ISO standard string format and uses that value:

DateCalculator::DateCalculator(const std::string &inEasterSunday)

{

  using namespace std::literals::chrono_literals;

  if ((inEasterSunday.length() != 10) || (inEasterSunday[4] != '-')

      || (inEasterSunday[7] != '-'))

    throw KalendarException("Bad format for Easter Sunday input",

                            inEasterSunday);

  std::string s = inEasterSunday.substr(0, 4);

  try

    {

      m_year = std::chrono::year{ std::stoi(s) };

      Easter = std::chrono::year_month_day{

        m_year,

        std::chrono::month{ static_cast<unsigned int>(

            std::stoi(inEasterSunday.substr(5, 2))) },

        std::chrono::day{

            static_cast<unsigned int>(std::stoi(inEasterSunday.substr(8, 2))) }

      };

      m_sundays.add(Easter, "Easter"s);

      {

        std::chrono::year_month_day Christmas(m_year, std::chrono::month{ 12 },

                                              25d);

        m_AdventSunday = AdventSunday{ Christmas };

      }

      completeInitialization();

    }

  catch (const std::exception &ex)

    {

      throw new KalendarException("Invalid values for Easter Sunday input",

                                  inEasterSunday + " " + ex.what());

    }

}

Note that AdventSunday does not take an explicit constructor in the first version.  That's because it also defaults to "this year".

.h file:

class AdventSunday

{

public:

  AdventSunday();

  AdventSunday(const std::chrono::year_month_day inChristmas);

  std::chrono::year_month_day getSundayNextBeforeAdvent() const { return { m_absoluteDay - std::chrono::days{ 7 } }; }

  std::chrono::year_month_day getAdventII() const { return { m_absoluteDay + std::chrono::days{ 7 } }; }

  std::chrono::year_month_day getAdventI() const { return m_day; }

  bool isBeforeAdvent(std::chrono::sys_days inDate) const { return inDate < m_absoluteDay; }

private:

  std::chrono::year_month_day m_day;

  std::chrono::sys_days m_absoluteDay;

};

.cpp file:

namespace

{

std::chrono::year_month_day

ChristmasToAdvent(std::chrono::year_month_day inVal)

{

  switch (std::chrono::weekday{ std::chrono::sys_days{ inVal } }.c_encoding())

    {

    case 5:

      return std::chrono::year_month_day{ inVal.year(),

                                          std::chrono::month{ 11 }, 29d };

      break;

    case 1:

      return std::chrono::year_month_day{ inVal.year(),

                                          std::chrono::month{ 12 }, 3d };

      break;

    case 6:

      return std::chrono::year_month_day{ inVal.year(),

                                          std::chrono::month{ 11 }, 28d };

      break;

    case 0:

      return std::chrono::year_month_day{ inVal.year(),

                                          std::chrono::month{ 11 }, 27d };

      break;

    case 4:

      return std::chrono::year_month_day{ inVal.year(),

                                          std::chrono::month{ 11 }, 30d };

      break;

    case 2:

      return std::chrono::year_month_day{ inVal.year(),

                                          std::chrono::month{ 12 }, 2d };

      break;

    case 3:

      return std::chrono::year_month_day{ inVal.year(),

                                          std::chrono::month{ 12 }, 1d };

      break;

    }

  return {};

}

}

...

AdventSunday::AdventSunday()

{

  const std::chrono::time_point now{ std::chrono::system_clock::now() };

  const std::chrono::year_month_day ymd{ std::chrono::floor<std::chrono::days>(now) };

  m_day = ChristmasToAdvent(

      std::chrono::year_month_day(ymd.year(), std::chrono::month{ 12 }, 25d));

  m_absoluteDay = std::chrono::sys_days{ m_day };

}

AdventSunday::AdventSunday(const std::chrono::year_month_day inChristmas):

    m_day{ ChristmasToAdvent(inChristmas) }, m_absoluteDay{

      std::chrono::sys_days{ m_day }

    }

}

We return the Sunday itself but also the two adjoining Sundays, which allows efficient  use of the sys_date version which we need for the comparison function as well.

The Easter date and the date of the beginning of Advent allow us to determine the dates for the Sundays of the year.  This is managed by the SundayIndex class.

class SundayIndex

{

public:

  SundayIndex() {}

  bool isSunday(const unsigned int inDay, const unsigned int inMonth) const

  {

    KalendarKey k{ inDay, inMonth };

    return m_sundays.contains(k);

  }

  std::string getSundayName(const unsigned int inDay,

                            const unsigned int inMonth) const

  {

    KalendarKey k{ inDay, inMonth };

    auto iter = m_sundays.find(k);

    if (iter == m_sundays.end())

      return {};

    return iter->second;

  }

  std::pair<std::string, std::string>

  getFerialName(const unsigned int inDay, const unsigned int inMonth,

                const std::chrono::year inYear) const;

  void add(const std::chrono::year_month_day inDay, const std::string &inName)

  {

    m_sundays.insert(

        std::make_pair(KalendarKey{ static_cast<unsigned>(inDay.day()),

                                    static_cast<unsigned>(inDay.month()) },

                       inName));

  }

  auto addLent(const std::chrono::year_month_day inPalmSunday)

      -> std::chrono::year_month_day;

  void addEpiphany(const std::chrono::year_month_day inFirst,

                   const std::chrono::year_month_day inSeptuagesima);

  void addPostEaster(const std::chrono::year_month_day inLowSunday,

                     const std::chrono::year_month_day inSNBA);

private:

  std::map<KalendarKey, std::string> m_sundays;

};

The three blocks of dates are handed their seeds by the date calculator:

auto SundayIndex::addLent(const std::chrono::year_month_day inPalmSunday)

    -> std::chrono::year_month_day

{

  std::chrono::year_month_day sunday{ inPalmSunday };

  std::ranges::for_each(std::ranges::iota_view(1, 9), [&](const int inVal) {

      sunday = std::chrono::year_month_day{ std::chrono::sys_days(sunday)

                                            - std::chrono::days{ 7 } };

      KalendarKey key{ static_cast<unsigned>(sunday.day()),

                       static_cast<unsigned>(sunday.month()) };

      switch (inVal)

        {

        case 1:

          m_sundays.insert(std::make_pair(key, "Passion Sunday"));

          break;

        case 2:

          m_sundays.insert(std::make_pair(key, "Laetare Sunday"));

          break;

        case 3:

          m_sundays.insert(std::make_pair(key, "Third Sunday in Lent"));

          break;

        case 4:

          m_sundays.insert(std::make_pair(key, "Second Sunday in Lent"));

          break;

        case 5:

          m_sundays.insert(std::make_pair(key, "First Sunday in Lent"));

          break;

        case 6:

          m_sundays.insert(std::make_pair(key, "Quinquagesima"));

          break;

        case 7:

          m_sundays.insert(std::make_pair(key, "Sexagesima"));

          break;

        case 8:

          m_sundays.insert(std::make_pair(key, "Septuagesima"));

          break;

        }

  });

  return sunday;

}

addLent() returns the calculated value of Septuagesima, which is a seasonal beginning point used more generally, including as an input to the generation of Epiphany:

void SundayIndex::addEpiphany(const std::chrono::year_month_day inFirst,

                              const std::chrono::year_month_day inSeptuagesima)

{

  int count = 1;

  std::chrono::year_month_day sunday = inFirst;

  auto sept = std::chrono::sys_days(inSeptuagesima);

  auto sd = std::chrono::sys_days(sunday);

  while ((sd < sept) && (count <= 5))

    {

      KalendarKey key{ static_cast<unsigned>(sunday.day()),

                       static_cast<unsigned>(sunday.month()) };

      switch (count++)

        {

        case 1:

          m_sundays.insert(

              std::make_pair(key, "Second Sunday after Epiphany"));

          break;

        case 2:

          m_sundays.insert(std::make_pair(key, "Third Sunday after Epiphany"));

          break;

        case 3:

          m_sundays.insert(

              std::make_pair(key, "Fourth Sunday after Epiphany"));

          break;

        case 4:

          m_sundays.insert(std::make_pair(key, "Fifth Sunday after Epiphany"));

          break;

        case 5:

          m_sundays.insert(std::make_pair(key, "Sixth Sunday after Epiphany"));

          break;

        }

      sunday = std::chrono::year_month_day{ std::chrono::sys_days(sunday)

                                            + std::chrono::days{ 7 } };

      sd = std::chrono::sys_days(sunday);

    }

}


void SundayIndex::addPostEaster(const std::chrono::year_month_day inLowSunday,

                                const std::chrono::year_month_day inSNBA)

{

  int count = 1;

  std::chrono::year_month_day sunday

      = { std::chrono::sys_days(inLowSunday) + std::chrono::days{ 7 } };

  int y = static_cast<int>(sunday.year());

  while (y == static_cast<int>(sunday.year()) && (sunday != inSNBA))

    {

      KalendarKey key{ static_cast<unsigned>(sunday.day()),

                       static_cast<unsigned>(sunday.month()) };

      switch (count++)

        {

        case 1:

          m_sundays.insert(std::make_pair(key, "Second Sunday after Easter"));

          break;

        case 2:

          m_sundays.insert(std::make_pair(key, "Third Sunday after Easter"));

          break;

        case 3:

          m_sundays.insert(std::make_pair(key, "Fourth Sunday after Easter"));

          break;

        case 4:

          m_sundays.insert(std::make_pair(key, "Rogation Sunday"));

          break;

        case 5:

          m_sundays.insert(std::make_pair(key, "Sunday after Ascension"));

          break;

        case 6:

          m_sundays.insert(std::make_pair(key, "Pentecost"));

          break;

        case 7:

          m_sundays.insert(std::make_pair(key, "Trinity Sunday"));

          break;

        case 8:

          m_sundays.insert(

              std::make_pair(key, "Second Sunday after Pentecost"));

          break;

        case 9:

          m_sundays.insert(

              std::make_pair(key, "Third Sunday after Pentecost"));

          break;

        case 10:

          m_sundays.insert(

              std::make_pair(key, "Fourth Sunday after Pentecost"));

          break;

        case 11:

          m_sundays.insert(

              std::make_pair(key, "Fifth Sunday after Pentecost"));

          break;

        case 12:

          m_sundays.insert(

              std::make_pair(key, "Sixth Sunday after Pentecost"));

          break;

        case 13:

          m_sundays.insert(

              std::make_pair(key, "Seventh Sunday after Pentecost"));

          break;

        case 14:

          m_sundays.insert(

              std::make_pair(key, "Eighth Sunday after Pentecost"));

          break;

        case 15:

          m_sundays.insert(

              std::make_pair(key, "Ninth Sunday after Pentecost"));

          break;

        case 16:

          m_sundays.insert(

              std::make_pair(key, "Tenth Sunday after Pentecost"));

          break;

        case 17:

          m_sundays.insert(

              std::make_pair(key, "Eleventh Sunday after Pentecost"));

          break;

        case 18:

          m_sundays.insert(

              std::make_pair(key, "Twelfth Sunday after Pentecost"));

          break;

        case 19:

          m_sundays.insert(

              std::make_pair(key, "Thirteenth Sunday after Pentecost"));

          break;

        case 20:

          m_sundays.insert(

              std::make_pair(key, "Fourteenth Sunday after Pentecost"));

          break;

        case 21:

          m_sundays.insert(

              std::make_pair(key, "Fifteenth Sunday after Pentecost"));

          break;

        case 22:

          m_sundays.insert(

              std::make_pair(key, "Sixteenth Sunday after Pentecost"));

          break;

        case 23:

          m_sundays.insert(

              std::make_pair(key, "Seventeenth Sunday after Pentecost"));

          break;

        case 24:

          m_sundays.insert(

              std::make_pair(key, "Eighteenth Sunday after Pentecost"));

          break;

        case 25:

          m_sundays.insert(

              std::make_pair(key, "Nineteenth Sunday after Pentecost"));

          break;

        case 26:

          m_sundays.insert(

              std::make_pair(key, "Twentieth Sunday after Pentecost"));

          break;

        case 27:

          m_sundays.insert(

              std::make_pair(key, "Twenty-First Sunday after Pentecost"));

          break;

        case 28:

          m_sundays.insert(

              std::make_pair(key, "Twenty-Second Sunday after Pentecost"));

          break;

        case 29:

          m_sundays.insert(

              std::make_pair(key, "Twenty-Third Sunday after Pentecost"));

          break;

        case 30:

          m_sundays.insert(

              std::make_pair(key, "Twenty-Fourth Sunday after Pentecost"));

          break;

        case 31:

          m_sundays.insert(

              std::make_pair(key, "Twenty-Fifth Sunday after Pentecost"));

          break;

        case 32:

          m_sundays.insert(

              std::make_pair(key, "Twenty-Sixth Sunday after Pentecost"));

          break;

        }

      sunday = std::chrono::year_month_day{ std::chrono::sys_days(sunday)

                                            + std::chrono::days{ 7 } };

    }

}

The naming follows Roman (or modern ICEL) use, rather than the Anglican names using "after Trinity".  The Calendar used assumes the old use of Septuagesima before Lent (as the breviary texts do).

The SundayIndex also manages the naming of ordinary ferias based on the preceding Sunday;

std::pair<std::string, std::string>

SundayIndex::getFerialName(const unsigned int inDay,

   const unsigned int inMonth,

   const std::chrono::year inYear) const

{

  std::chrono::year_month_day date{ inYear, std::chrono::month{ inMonth },

    std::chrono::day{ inDay } };

  std::string dayName;

  int diff = 0;

  auto wday = std::chrono::weekday(date);

  if (wday == std::chrono::Sunday)

    {

      auto n = getSundayName(inDay, inMonth);

      return std::make_pair(n, n);

    }

  else if (wday == std::chrono::Monday)

    {

      diff = -1;

      dayName = "Monday";

    }

  else if (wday == std::chrono::Tuesday)

    {

      diff = -2;

      dayName = "Tuesday";

    }

  else if (wday == std::chrono::Wednesday)

    {

      diff = -3;

      dayName = "Wednesday";

    }

  else if (wday == std::chrono::Thursday)

    {

      diff = -4;

      dayName = "Thursday";

    }

  else if (wday == std::chrono::Friday)

    {

      diff = -5;

      dayName = "Friday";

    }

  else if (wday == std::chrono::Saturday)

    {

      diff = -6;

      dayName = "Saturday";

    }

  std::chrono::year_month_day sunday_date{ std::chrono::sys_days(date)

    + std::chrono::days{ diff } };

  auto n = getSundayName(static_cast<unsigned>(sunday_date.day()),

                                   static_cast<unsigned>(sunday_date.month()));

  if (n.find("Palm Sunday") != std::string::npos)

    {

      if (dayName == "Thursday")

return std::make_pair("Maundy Thursday", n);

      else if (dayName == "Friday")

return std::make_pair("Good Friday", n);

      if (dayName == "Saturday")

return std::make_pair("Holy Saturday", n);

      else

return std::make_pair((dayName + " in Holy Week"), n);

    }

  else if (n == "Easter")

    {

      return std::make_pair((dayName + " in Easter Week"), n);

    }

  if (n == "First Sunday after Christmas")

    {

      return std::make_pair((dayName + " after Christmas I"), n);

    }

  else if (n == "Second Sunday after Christmas")

    {

      return std::make_pair((dayName + " after Christmas II"), n);

    }

  else if ((n.find("Sunday after Epiphany") != std::string::npos)

      || (n.find("Sunday after Pentecost") != std::string::npos)

      || (n.find("Sunday after Easter") != std::string::npos)

      || (n.find("Sunday in Advent") != std::string::npos)

      || (n.find("Sunday in Lent") != std::string::npos))

    {

      std::string season("Pentecost ");

      if (n.find("Sunday after Epiphany") != std::string::npos)

        season = "Epiphany ";

      else if (n.find("Sunday in Advent") != std::string::npos)

        season = "Advent ";

      else if (n.find("Sunday after Easter") != std::string::npos)

        season = "Easter ";

      else if (n.find("Sunday in Lent") != std::string::npos)

        season = "Lent ";

      static const std::vector<std::pair<std::string, std::string>> numerals{

        { "First", "I" },

        { "Second", "II" },

        { "Third", "III" },

        { "Fourth", "IV" },

        { "Fifth", "V" },

        { "Sixth", "VI" },

        { "Seventh", "VII" },

        { "Eighth", "VIII" },

        { "Ninth", "IX" },

        { "Tenth", "X" },

        { "Eleventh", "XI" },

        { "Twelfth", "XII" },

        { "Thirteenth", "XIII" },

        { "Fourteenth", "XIV" },

        { "Fifteenth", "XV" },

        { "Sixteenth", "XVI" },

        { "Seventeenth", "XVII" },

        { "Eighteenth", "XVIII" },

        { "Nineteenth", "XIX" },

        { "Twentieth", "XX" },

        { "Twenty-first", "XI" },

        { "Twenty-second", "XXII" },

        { "Twenty-third", "XXIII" },

        { "Twenty-fourth", "XXIV" },

        { "Twenty-fifth", "XXV" },

        { "Twenty-sixth", "XXVI" }

      };

      if (auto iter

          = std::ranges::find_if(numerals,

                                 [&n](const auto &inPair) {

                                   return n.starts_with(inPair.first);

                                 });

          iter != numerals.end())

        return std::make_pair((dayName + " after " + season + iter->second),

                              n);

    }

  return std::make_pair((dayName + " after " + n), n);

}

That loop in determining the ferial name is not efficient, but it would be called once during program startup in a normal use context.  If it were to need to be faster, it would be fairly easy, though a bit fussier, to break the vector numerals into several vectors based on the beginning letter and then switch on that beginning letter to reduce the number of comparisons.

The final building block is the managing of octaves.  Here again, the use follows the old Calendar.

class Octave

{

public:

  Octave() {}

  Octave(const unsigned int inDay, const unsigned int inMonth,

         const std::string &inName, const bool inGreater):

      m_name(inName),

      m_day(inDay), m_month(inMonth), m_greaterOctave(inGreater)

  { }

  std::pair<std::chrono::year_month_day, std::chrono::year_month_day>

  getDays(const int inYear) const

  {

    std::chrono::year_month_day begin{ std::chrono::year{ inYear },

                                       std::chrono::month{ m_month },

                                       std::chrono::day{ m_day } };


    std::chrono::year_month_day end{ std::chrono::sys_days{begin} + std::chrono::days{7} };

    return std::make_pair(begin, end);

  }

  const std::string &getName() const { return m_name; }

  bool isGreater() const { return m_greaterOctave; }

private:

  std::string m_name;

  unsigned int m_day = 1;

  unsigned int m_month = 1;

  bool m_greaterOctave = false;

};

class OctaveSet

{

public:

  OctaveSet() {}


  OctaveSet(const unsigned int inCCDay, const unsigned int inCCMonth,

            const std::chrono::year_month_day &inBeginHolyWeek, int inYear);


  Feast

  isInOctave(const std::chrono::year_month_day &inDay) const;


private:

  std::vector<Octave> m_octaves;

};

As there are only a limited number of octaves, they are hardcoded into the constructor of the OctaveSet.

OctaveSet::OctaveSet(const unsigned int inCCDay, const unsigned int inCCMonth,

                     const std::chrono::year_month_day &inBeginHolyWeek,

                     const int inYear)

{

  using namespace std::literals::chrono_literals;


  auto PalmSundayDays = std::chrono::sys_days{ inBeginHolyWeek };

  std::chrono::year_month_day endEasterWeek{ PalmSundayDays

                                             + std::chrono::days{ 14 } };

  std::chrono::year_month_day Easter{ PalmSundayDays

                                      + std::chrono::days{ 7 } };

  std::chrono::year_month_day SacredHeart{ PalmSundayDays

                                           + std::chrono::days{ 75 } };


  m_octaves.emplace_back(6, 1, "Epiphany", true);

  m_octaves.emplace_back(2, 2, "Purification", true);

  std::chrono::year_month_day annunciation(std::chrono::year{ inYear },

                                           std::chrono::month{ 3 }, 25d);

  bool easterInserted = false;

  if (std::chrono::sys_days{ annunciation }

      > std::chrono::sys_days{ inBeginHolyWeek })

    {

      annunciation = std::chrono::year_month_day{

        std::chrono::sys_days{ inBeginHolyWeek } + std::chrono::days{ 15 }

      };

      unsigned int aday = static_cast<unsigned>(annunciation.day());

      if ((aday == 3) || (aday == 4))

        aday = 5; // Shift to avoid Saint's days

      if (aday > 25)

        m_octaves.emplace_back(aday, 3, "Annunciation"s, true);

      else

        {

          easterInserted = true;

          m_octaves.emplace_back(static_cast<unsigned>(Easter.day()),

                                 static_cast<unsigned>(Easter.month()),

                                 "Easter"s, true);

          m_octaves.emplace_back(aday, 4, "Annunciation"s, true);

        }

    }

  else

    m_octaves.emplace_back(25, 3, "Annunciation"s, true);

  if (!easterInserted)

    m_octaves.emplace_back(static_cast<unsigned>(Easter.day()),

                           static_cast<unsigned>(Easter.month()), "Easter"s,

                           true);

  std::chrono::year_month_day ascension{

    std::chrono::sys_days{ inBeginHolyWeek } + std::chrono::days{ 46 }

  };

  m_octaves.emplace_back(static_cast<unsigned>(ascension.day()),

                         (ascension.month() == std::chrono::month{ 4 }) ? 4

                                                                        : 5,

                         "Ascension Day"s, true);

  std::chrono::year_month_day pentecost{ std::chrono::sys_days{ ascension }

                                         + std::chrono::days{ 10 } };

  m_octaves.emplace_back(static_cast<unsigned>(pentecost.day()),

                         (pentecost.month() == std::chrono::month{ 5 }) ? 5

                                                                        : 6,

                         "Pentecost"s, true);

  m_octaves.emplace_back(inCCDay, inCCMonth, "Corpus Christi"s, true);

  m_octaves.emplace_back(static_cast<unsigned>(SacredHeart.day()),

                         static_cast<unsigned>(SacredHeart.month()),

                         "Sacred Heart"s, true);

  m_octaves.emplace_back(24, 6, "Nativity of St. John Baptist"s, false);

  m_octaves.emplace_back(29, 6, "SS. Peter and Paul, App"s, false);

  m_octaves.emplace_back(2, 7, "Visitation of the Blessed Virgin Mary"s, true);

  m_octaves.emplace_back(7, 8, "Holy Name"s, true);

  m_octaves.emplace_back(15, 8, "Assumption of the Blessed Virgin Mary"s,

                         true);

  m_octaves.emplace_back(8, 9, "Nativity of the Blessed Virgin Mary"s, true);

  m_octaves.emplace_back(29, 9, "St. Michael and All Angels"s, true);

  m_octaves.emplace_back(1, 11, "All Saints"s, true);

  m_octaves.emplace_back(

      8, 12, "Immaculate Conception of the Blessed Virgin Mary"s, true);

  m_octaves.emplace_back(25, 12, "Christmas"s, true);

  m_octaves.emplace_back(26, 12, "St. Stephen"s, true);

  m_octaves.emplace_back(27, 12, "St. John the Evangelist"s, true);

  m_octaves.emplace_back(27, 12, "Holy Innocents"s, true);

}

Now that we have looked at all the building blocks, we can look at the rest of the initialization of the Date Calculator, shared between the constructors:

void DateCalculator::completeInitialization()

{

  std::chrono::year_month_day PalmSunday{ std::chrono::sys_days(Easter)

    - std::chrono::days{ 7 } };

  std::chrono::year_month_day LowSunday{ std::chrono::sys_days(Easter)

    + std::chrono::days{ 7 } };

  m_beginHolyWeek

    = KalendarKey{ static_cast<unsigned>(PalmSunday.day()),

    static_cast<unsigned>(PalmSunday.month()) };

  m_sundays.add(PalmSunday, "Palm Sunday"s);

  m_endEasterWeek

    = KalendarKey{ static_cast<unsigned>(LowSunday.day()),

    static_cast<unsigned>(LowSunday.month()) };

  m_sundays.add(LowSunday, "First Sunday after Easter"s);

  m_sundays.add(m_AdventSunday.getAdventI(), "First Sunday in Advent"s);

  m_sundays.add(m_AdventSunday.getSundayNextBeforeAdvent(),

"Sunday Next Before Advent"s);

  m_sundays.add(m_AdventSunday.getAdventII(), "Second Sunday in Advent"s);

  std::chrono::year_month_day as{ std::chrono::sys_days(

m_AdventSunday.getAdventII())

    + std::chrono::days{ 7 } };

  m_sundays.add(as, "Third Sunday in Advent"s);

  as = std::chrono::year_month_day{ std::chrono::sys_days(as)

    + std::chrono::days{ 7 } };

  if (static_cast<unsigned>(as.day()) == 24)

    m_sundays.add(as, "Christmas Eve"s);

  else

    m_sundays.add(as, "Fourth Sunday in Advent"s);

  as = std::chrono::year_month_day{ std::chrono::sys_days(as)

    + std::chrono::days{ 7 } };

  if (static_cast<unsigned>(as.day()) == 25)

    m_sundays.add(as, "Christmas Day"s);

  else

    m_sundays.add(as, "First Sunday after Christmas"s);

  Septuagesima = m_sundays.addLent(PalmSunday);

  std::chrono::year_month_day first{ m_year, std::chrono::month{ 1 },

    std::chrono::day{ 1 } };

  if (std::chrono::weekday(first) == std::chrono::Sunday)

    {

      m_sundays.add(first, "First Sunday after Christmas");

      first = std::chrono::year_month_day{ std::chrono::sys_days(first)

+ std::chrono::days{ 7 } };

      m_sundays.add(first, "Baptism of Our Lord");

    }

  else

    {

      while (std::chrono::weekday(first) != std::chrono::Sunday)

{

  first = std::chrono::year_month_day{ std::chrono::sys_days(first)

    + std::chrono::days{ 1 } };

}

      auto fday = static_cast<unsigned>(first.day());

      if (fday == 7)

{

  m_sundays.add(first, "Baptism of Our Lord");

}

      else

{

  if (fday < 6)

    {

      m_sundays.add(first, "Second Sunday after Christmas");

    }

  else if (fday == 6)

    {

      m_sundays.add(first, "Epiphany");

    }

  first = std::chrono::year_month_day{ std::chrono::sys_days(first)

    + std::chrono::days{ 7 } };


  m_sundays.add(first, "Baptism of Our Lord");

}

    }

  // First now points to baptism of the Lord, which has been inserted.

  // Fill in down to the beginning of the Easter cycle

  first = std::chrono::year_month_day{ std::chrono::sys_days(first)

    + std::chrono::days{ 7 } };

  m_sundays.addEpiphany(first, Septuagesima);


  m_sundays.addPostEaster(LowSunday,

  m_AdventSunday.getSundayNextBeforeAdvent());


  LadyDay = KalendarKey{ 25, 3 };

  if (((LadyDay >= m_beginHolyWeek) && (LadyDay <= m_endEasterWeek)))

    {

      std::chrono::year_month_day ld{ std::chrono::sys_days(LowSunday)

+ std::chrono::days{ 1 } };

      LadyDay = KalendarKey{ static_cast<unsigned>(ld.day()),

static_cast<unsigned>(ld.month()) };

    }

  CorpusChristi

    = std::chrono::year_month_day{ std::chrono::sys_days(Easter)

    + std::chrono::days{ 60 } };

  SacredHeart = std::chrono::year_month_day{ std::chrono::sys_days(Easter)

    + std::chrono::days{ 68 } };

  m_octaves = OctaveSet(

static_cast<unsigned>(CorpusChristi.day()),

static_cast<unsigned>(CorpusChristi.month()),

std::chrono::year_month_day{ std::chrono::sys_days(Easter)

  - std::chrono::days{ 7 } },

static_cast<int>(m_year));

}

Proper of Saints

The Proper of Saints is overall somewhat simpler: it's a table of feasts with tweaks to handle precedence with Sundays and with the Easter block's precedence.

The core storage has a simple interface:

class ISaintsDaySet

{

public:

  virtual ~ISaintsDaySet();

  virtual Feast getFeast(const unsigned int inDay,

                         const unsigned int inMonth) const = 0;

  virtual void add(const unsigned int inDay, const unsigned int inMonth,

                   const std::string &inName, const Grades inGrade)

      = 0;

  virtual void remove(const unsigned int inDay, const unsigned int inMonth) = 0;

  virtual void write(std::ostream& outStream) const = 0;

};

This supports adding a record, removing a record, and writing out the record set in some string-encoded format.

(A feast is a very simple struct:

struct Feast

{

  std::string name;

  Grades grade = Grades::FERIA;

};

).

The primary implementation is almost as simple:

class SaintsDaySet : public ISaintsDaySet

{

public:

  ~SaintsDaySet() override;

  Feast getFeast(const unsigned int inDay,

                 const unsigned int inMonth) const override

  {

    auto iter = m_feasts.find(KalendarKey{ inDay, inMonth });

    if (iter == m_feasts.end())

      return {};

    return iter->second;

  }

  void add(const unsigned int inDay, const unsigned int inMonth,

           const std::string &inName, const Grades inGrade) override

  {

    m_feasts.insert(

      std::make_pair(KalendarKey(inDay, inMonth), Feast(inName, inGrade)));

  }

  void remove(const unsigned int inDay, const unsigned int inMonth) override

  {

    m_feasts.erase(KalendarKey(inDay, inMonth));

  }

  void write(std::ostream& outStream) const override;

private:

  std::map<KalendarKey, Feast> m_feasts;

};

The default format is effectively a line-record format, although it also conforms to XML standards:

void SaintsDaySet::write(std::ostream &outStream) const

{

  outStream << "<FEASTS>\n";

  std::ranges::for_each(m_feasts, [&outStream](const auto &inVal) {

    outStream << "<FEAST day=\"" << inVal.first.day << "\" month=\""

              << inVal.first.month << "\" name=\"" << inVal.second.name

              << "\" grade=\""

              << inVal.second.grade << "\"/>\n";

  });

  outStream << "</FEASTS>" << std::endl;

}

Unfortunately, we can't use that directly, because interactions with the proper of time may shift some dates.  In addition, we can initialize a range of standard feasts.  So we have a derived class:

class BaseSaintsDaySet : public SaintsDaySet

{

public:

  BaseSaintsDaySet(const IDateCalculator &inCalculator);

  ~BaseSaintsDaySet() override;

  void add(const unsigned int inDay, const unsigned int inMonth,

           const std::string &inName, const Grades inGrade) override

  {

    addFeast(m_calculator, inName, inGrade, inDay, inMonth);

  }

private:

  const IDateCalculator &m_calculator;


  void addFeast(const IDateCalculator &inCalculator, const std::string &inName,

                const Grades inGrade, const unsigned int inDay,

                const unsigned int inMonth);

};

addFeast() manages the interaction with the proper of time:

void BaseSaintsDaySet::addFeast(const IDateCalculator &inCalculator,

                                const std::string &inName,

                                const Grades inGrade, const unsigned int inDay,

                                const unsigned int inMonth)

{

  if (((static_cast<int>(inGrade)

        < static_cast<int>(Grades::DOUBLE_SECOND_CLASS))

       || (inName

           == "St. Joseph")) // Always will compete with Sunday of first class

      && inCalculator.isSunday(inDay, inMonth))

    {

      int day = inDay + 1;

      int month = inMonth;

      if ((inDay == 31)

          || ((inDay == 30)

              && ((inMonth == 4) || (inMonth == 6) || (inMonth == 9)

                  || (inMonth == 11))))

        {

          day = 1;

          month += 1;

        }

      SaintsDaySet::add(day, month, inName, inGrade);

    }

  else if (((inMonth == 3) || (inMonth == 4))

           && inCalculator.isInEasterPrivilege(inDay, inMonth))

    return;

  else

    SaintsDaySet::add(inDay, inMonth, inName, inGrade);

}

and the constructor provides the baseline set of feasts:

BaseSaintsDaySet::BaseSaintsDaySet(const IDateCalculator &inCalculator):

  m_calculator(inCalculator)

{

  addFeast(inCalculator, "Mary, Mother of God", Grades::DOUBLE, 1, 1);

  addFeast(inCalculator, "Epiphany", Grades::DOUBLE_FIRST_CLASS, 6, 1);

  addFeast(inCalculator, "St. Hilary, B.C.D.", Grades::DOUBLE, 14, 1);

  addFeast(inCalculator, "St. Paul, First Hermit, C.", Grades::DOUBLE, 15, 1);

  addFeast(inCalculator, "St. Anthony, Ab.", Grades::DOUBLE, 17, 1);

  addFeast(inCalculator, "Chair of St. Peter at Rome", Grades::GREATER_DOUBLE,

           18, 1);

  addFeast(inCalculator, "SS. Fabian & Sebastian, MM.", Grades::DOUBLE, 20, 1);

  addFeast(inCalculator, "St. Agnes, V.M.", Grades::DOUBLE, 21, 1);

  addFeast(inCalculator, "SS. Vincent & Anastasius, MM.", Grades::SEMIDOUBLE,

           22, 1);

  addFeast(inCalculator, "St. Timothy, B.M.", Grades::DOUBLE, 24, 1);

  addFeast(inCalculator, "Conversion of St. Paul", Grades::DOUBLE_SECOND_CLASS,

           25, 1);

  addFeast(inCalculator, "St. Polycarp, B.M.", Grades::DOUBLE, 26, 1);

  addFeast(inCalculator, "St. John Chrysostom, B.C.D.", Grades::DOUBLE, 27, 1);

  addFeast(inCalculator, "St. Francis de Sales, B.C.D.", Grades::DOUBLE, 29,

           1);

  addFeast(inCalculator, "St. Ignatius, B.M.", Grades::DOUBLE, 1, 2);

  addFeast(inCalculator, "Purification of the Virgin",

           Grades::DOUBLE_FIRST_CLASS, 2, 2);

  addFeast(inCalculator, "St. Blaise, B.M.", Grades::SIMPLE, 3, 2);

  addFeast(inCalculator, "St. Agatha, V.M.", Grades::DOUBLE, 5, 2);

  addFeast(inCalculator, "St. Titus, B.C.", Grades::DOUBLE, 6, 2);

  addFeast(inCalculator, "St. Cyril of Alexandria, B.C.D.", Grades::DOUBLE, 9,

           2);

  addFeast(inCalculator, "St. Scholastica, V.", Grades::DOUBLE, 10, 2);

  addFeast(inCalculator, "St. Peter's Chair at Antioch",

           Grades::GREATER_DOUBLE, 22, 2);

  addFeast(inCalculator, "St. Peter Damian, B.C.D.", Grades::DOUBLE, 23, 2);

  addFeast(inCalculator, "St. Matthias, Ap.", Grades::DOUBLE_SECOND_CLASS, 24,

           2);

  addFeast(inCalculator, "SS. Perpetua and Felicity, MM.", Grades::DOUBLE, 7,

           3);

  addFeast(inCalculator, "St. Thomas Aquinas, C.D.", Grades::DOUBLE, 7, 3);

  addFeast(inCalculator, "St. Joseph", Grades::DOUBLE_SECOND_CLASS, 19, 3);

  addFeast(inCalculator, "St. Benedict, Ab.", Grades::GREATER_DOUBLE, 21, 3);

  addFeast(inCalculator, "St. Isidore, B.C.D.", Grades::DOUBLE, 4, 4);

  addFeast(inCalculator, "St. Leo the Great, P.C.D.", Grades::DOUBLE, 11, 4);

  addFeast(inCalculator, "St. Justin Martyr, M.", Grades::DOUBLE, 14, 4);

  addFeast(inCalculator, "St. Anselm, B.C.D.", Grades::DOUBLE, 21, 4);

  addFeast(inCalculator, "St. George, M.", Grades::SEMIDOUBLE, 23, 4);

  addFeast(inCalculator, "St. Mark, Ev.", Grades::DOUBLE_SECOND_CLASS, 25, 4);

  addFeast(inCalculator, "St. Catherine of Siena, V.D.", Grades::DOUBLE, 30,

           4);

  addFeast(inCalculator, "SS. Philip & James, App.",

           Grades::DOUBLE_SECOND_CLASS, 1, 5);

  addFeast(inCalculator, "St. Athanasius, B.C.D.", Grades::DOUBLE, 2, 5);

  addFeast(inCalculator, "Invention of the Holy Cross",

           Grades::DOUBLE_SECOND_CLASS, 3, 5);

  addFeast(inCalculator, "St. Monica, W.", Grades::DOUBLE, 4, 5);

  addFeast(inCalculator, "St. John ad Portam Latinam", Grades::DOUBLE, 6, 5);

  addFeast(inCalculator, "St. Stanislas, B.M.", Grades::DOUBLE, 7, 5);

  addFeast(inCalculator, "St. Gregory of Nazianzus, B.C.D.", Grades::DOUBLE, 9,

           5);

  addFeast(inCalculator, "St. Bernardin of Siena, C.", Grades::DOUBLE, 20, 5);

  addFeast(inCalculator, "St. Augustine of Canterbury, B.C.",

           Grades::GREATER_DOUBLE, 26, 5);

  addFeast(inCalculator, "St. Venerable Bede, C.", Grades::DOUBLE, 27, 5);

  addFeast(inCalculator, "St. Boniface, B.M.", Grades::DOUBLE, 5, 6);

  addFeast(inCalculator, "St. Margaret, W.", Grades::DOUBLE, 10, 6);

  addFeast(inCalculator, "St. Barnabas, Ap.", Grades::DOUBLE_SECOND_CLASS, 11,

           6);

  addFeast(inCalculator, "St. Anthony of Padua, C.", Grades::DOUBLE, 13, 6);

  addFeast(inCalculator, "St. Basil the Great, B.C.D.", Grades::DOUBLE, 14, 6);

  addFeast(inCalculator, "St. Alban, M.", Grades::DOUBLE, 22, 6);

  addFeast(inCalculator, "Nativity of St. John the Baptist",

           Grades::DOUBLE_FIRST_CLASS, 24, 6);

  addFeast(inCalculator, "St. Iranaeus, B.M.", Grades::DOUBLE, 28, 6);

  addFeast(inCalculator, "SS. Peter and Paul, App.",

           Grades::DOUBLE_FIRST_CLASS, 29, 6);

  addFeast(inCalculator, "Precious Blood of Our Lord",

           Grades::DOUBLE_FIRST_CLASS, 1, 7);

  addFeast(inCalculator, "Visitation of the Blesssed Virgin Mary",

           Grades::DOUBLE_SECOND_CLASS, 2, 7);

  addFeast(inCalculator, "SS. Cyril and Methodius, BB.CC.", Grades::DOUBLE, 7,

           7);

  addFeast(inCalculator, "SS. Bonaventure, B.C.D.", Grades::DOUBLE, 14, 7);

  addFeast(inCalculator, "St. Vincent de Paul, Conf.", Grades::DOUBLE, 19, 7);

  addFeast(inCalculator, "St. James, Ap.", Grades::DOUBLE_SECOND_CLASS, 25, 7);

  addFeast(inCalculator, "St. Mary Magdalene", Grades::DOUBLE_SECOND_CLASS, 22,

           7);

  addFeast(inCalculator, "St. Anne, Mother of the B.V.M.",

           Grades::DOUBLE_SECOND_CLASS, 26, 7);

  addFeast(inCalculator, "St. Martha, V.", Grades::DOUBLE, 29, 7);

  addFeast(inCalculator, "St. Ignatius Loyola, C.", Grades::GREATER_DOUBLE, 31,

           7);

  addFeast(inCalculator, "St. Peter's Chains", Grades::GREATER_DOUBLE, 1, 8);

  addFeast(inCalculator, "St. Alphonsus Liguori, B.C.D.", Grades::DOUBLE, 2,

           8);

  addFeast(inCalculator, "St. Dominic, C.", Grades::GREATER_DOUBLE, 4, 8);

  addFeast(inCalculator, "Transfiguration", Grades::DOUBLE_SECOND_CLASS, 6, 8);

  addFeast(inCalculator, "Holy Name of Jesus", Grades::DOUBLE, 7, 8);

  addFeast(inCalculator, "St. Lawrence, M.", Grades::DOUBLE_SECOND_CLASS, 10,

           8);

  addFeast(inCalculator, "St. Clare, V.", Grades::DOUBLE, 12, 8);

  addFeast(inCalculator, "Assumption of the Blessed Virgin Mary",

           Grades::DOUBLE_FIRST_CLASS, 15, 8);

  addFeast(inCalculator, "St. Bernard, Ab.C.D.", Grades::DOUBLE, 20, 8);

  addFeast(inCalculator, "St. Jane Frances, W.", Grades::DOUBLE, 21, 8);

  addFeast(inCalculator, "St. Bartholomew, Ap.", Grades::DOUBLE_SECOND_CLASS,

           24, 8);

  addFeast(inCalculator, "St. Louis, K.C.", Grades::SEMIDOUBLE, 25, 8);

  addFeast(inCalculator, "St. Augustine, B.C.D.", Grades::DOUBLE, 28, 8);

  addFeast(inCalculator, "Beheading of St. John Baptist",

           Grades::GREATER_DOUBLE, 29, 8);

  addFeast(inCalculator, "St. Rose of Lima, V.", Grades::DOUBLE, 30, 8);

  addFeast(inCalculator, "St. Aidan, B.C.", Grades::DOUBLE, 31, 8);

  addFeast(inCalculator, "St. Stephen, K.C.", Grades::SEMIDOUBLE, 31, 8);

  addFeast(inCalculator, "Nativity of the Blessed Virgin Mary",

           Grades::DOUBLE_SECOND_CLASS, 8, 9);

  addFeast(inCalculator, "Exaltation of the Holy Cross",

           Grades::GREATER_DOUBLE, 14, 9);

  addFeast(inCalculator, "Seven Sorrows of the B.V.M.",

           Grades::DOUBLE_SECOND_CLASS, 15, 9);

  addFeast(inCalculator, "Saint Matthew, Ap.", Grades::DOUBLE_SECOND_CLASS, 21,

           9);

  addFeast(inCalculator, "Saint Cosmas & Damian, MM.", Grades::SEMIDOUBLE, 27,

           9);

  addFeast(inCalculator, "Saint Michael and All Angels",

           Grades::DOUBLE_SECOND_CLASS, 29, 9);

  addFeast(inCalculator, "Holy Guardian Angels", Grades::GREATER_DOUBLE, 2,

           10);

  addFeast(inCalculator, "St. Francis of Assissi, C.", Grades::GREATER_DOUBLE,

           4, 10);

  addFeast(inCalculator, "Holy Rosary of the B.V.M.",

           Grades::DOUBLE_SECOND_CLASS, 7, 10);

  addFeast(inCalculator, "St. Teresa, V.D.", Grades::DOUBLE, 15, 10);

  addFeast(inCalculator, "SS. Luke, Ev.", Grades::DOUBLE_SECOND_CLASS, 18, 10);

  addFeast(inCalculator, "SS. Simon and Jude, App.",

           Grades::DOUBLE_SECOND_CLASS, 28, 10);

  addFeast(inCalculator, "All Saints", Grades::DOUBLE_FIRST_CLASS, 1, 11);

  addFeast(inCalculator, "St. Charles Borromeo, B.C.", Grades::DOUBLE, 4, 11);

  addFeast(inCalculator, "Dedication of the Archbasilica of Our Saviour",

           Grades::DOUBLE_SECOND_CLASS, 9, 11);

  addFeast(inCalculator, "St. Martin, B.C.", Grades::DOUBLE, 11, 11);

  addFeast(inCalculator, "St. Elizabeth of Hungary, W.", Grades::DOUBLE, 19,

           11);

  addFeast(inCalculator, "Presentation of the B.V.M.", Grades::GREATER_DOUBLE,

           21, 11);

  addFeast(inCalculator, "St. Cecelia, V.M.", Grades::DOUBLE, 22, 11);

  addFeast(inCalculator, "St. Clement I, P.M.", Grades::DOUBLE, 23, 11);

  addFeast(inCalculator, "St. John of the Cross, C.D.", Grades::DOUBLE, 24,

           11);

  addFeast(inCalculator, "St. Catherine, V.M.", Grades::DOUBLE, 25, 11);

  addFeast(inCalculator, "St. Andrew, Ap.", Grades::DOUBLE_SECOND_CLASS, 25,

           30);

  addFeast(inCalculator, "St. Francis Xavier, C.", Grades::GREATER_DOUBLE, 3,

           12);

  addFeast(inCalculator, "St. Nicholas, B.C.", Grades::DOUBLE, 6, 12);

  addFeast(inCalculator, "St. Ambrose, B.C.D.", Grades::DOUBLE, 7, 12);

  addFeast(inCalculator, "Immaculate Conception of the Blessed Virgin Mary",

           Grades::DOUBLE_FIRST_CLASS, 8, 12);

  addFeast(inCalculator, "St. Lucy, V.M.", Grades::DOUBLE, 13, 12);

  addFeast(inCalculator, "St. Thomas, Ap.", Grades::DOUBLE_SECOND_CLASS, 21,

           12);

  addFeast(inCalculator, "Christmas Eve", Grades::PRIVILEGED_VIGIL, 24, 12);

  addFeast(inCalculator, "Christmas", Grades::DOUBLE_FIRST_CLASS, 25, 12);

  addFeast(inCalculator, "St. Stephen, Protomartyr",

           Grades::DOUBLE_SECOND_CLASS, 26, 12);

  addFeast(inCalculator, "St. John, Ap. Ev.", Grades::DOUBLE_SECOND_CLASS, 27,

           12);

  addFeast(inCalculator, "Holy Innocents", Grades::DOUBLE_SECOND_CLASS, 28,

           12);

  addFeast(inCalculator, "St. Thomas of Canterbury", Grades::DOUBLE, 29, 12);

  addFeast(inCalculator, "St. Sylvester I, P.C.", Grades::DOUBLE, 31, 12);

}

This class is used for the unit tests, as it should be stable over time.

For full use, we will want to be able to configure the list, normally by adding feasts, but sometimes by removing them or moving them around (Holy Name is in the list based on the Sarum calendar; Rome has it on January 3 (at one time, on the Feast of the Circumcision (Anglican), but that is now Mary, Mother of God, at least in North America).

So we have a further extension to the class:

class ExtensibleSaintsDaySet : public BaseSaintsDaySet

{

public:

  ExtensibleSaintsDaySet(const IDateCalculator &inCalculator):

      BaseSaintsDaySet(inCalculator)

  { }

  ~ExtensibleSaintsDaySet() override;

  void extendFromStream(std::istream &inStream, const Log::ILogger &inLogger);

private:

  static std::string ExtractAttribute(std::string_view inVal,

                                      const std::string &inName);

};

This starts with the basic set but can be tweaked via a stream (either a file or in-memory source):

void ExtensibleSaintsDaySet::extendFromStream(std::istream &inStream,

                                              const Log::ILogger &inLogger)

{

  while (true)

    {

      std::string str;

      std::getline(inStream, str);

      if (str.empty())

        {

          break;

        }

      if (str.starts_with("<FEASTS>"))

        continue;

      else if (str.starts_with("</FEASTS>"))

        break;

      else if (str.starts_with("<FEAST "))

        {

          auto name = ExtractAttribute(str, "name"s);

          auto day = ExtractAttribute(str, "day"s);

          auto month = ExtractAttribute(str, "month"s);

          auto grade = ExtractAttribute(str, "grade"s);

          if (name.empty() || day.empty() || month.empty() || grade.empty())

            inLogger.log(Log::Severity::Error,

                         "Irregular/invalid FEAST element: " + str);

          else

            {

              try

                {

                  add(static_cast<unsigned int>(std::stoi(day)),

      static_cast<unsigned int>(std::stoi(month)),

      name, GradeFromString(grade));

                }

              catch (const std::exception& ex)

                {

                  inLogger.log(Log::Severity::Error,

                               "Irregular/invalid FEAST element: " + str);

                }

            }

        }

      else if (str.starts_with("<REMOVE "))

        {

          auto day = ExtractAttribute(str, "day"s);

          auto month = ExtractAttribute(str, "month"s);

          if (day.empty() || month.empty())

            inLogger.log(Log::Severity::Error,

                         "Irregular/invalid REMOVE element: " + str);

          else

            {

              try

                {

                  remove(static_cast<unsigned int>(std::stoi(day)),

                         static_cast<unsigned int>(std::stoi(month)));

                }

              catch (const std::exception& ex)

                {

                  inLogger.log(Log::Severity::Error,

                               "Irregular/invalid REMOVE element: " + str);

                }

            }

        }

    }

}

This makes no pretence to do a full XML parse, but it does do some validation.  The ExtractAttribute() helper function just searches through the line record:

std::string ExtensibleSaintsDaySet::ExtractAttribute(std::string_view inVal,

                                                     const std::string &inName)

{

  auto index = inVal.find(inName + "=\"");

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

    return {};

  inVal.remove_prefix(index + inName.length() + 2);

  index = inVal.find("\"");

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

    return {};

  return std::string { inVal.substr(0, index) };

}

The unit test shows the two typical types of use of the ability.  First, changing the date of a feast:

  TestDateCalculator calc;

  BreviaryLib::ExtensibleSaintsDaySet theSet(calc);

...

  BreviaryLib::Feast f = theSet.getFeast(7, 8);

  BOOST_CHECK_EQUAL(f.name, "Holy Name of Jesus"s);

  // Change Holy Name to Roman Date

  std::string text(

   R"(<FEASTS>

<REMOVE day="7" month="8"/> 

<FEAST day="3" month="1" name="Holy Name of Jesus" grade="DOUBLE_SECOND_CLASS"/>

</FEASTS>

)");


  std::istringstream str(text);

  Log::TestLogger logger;

  theSet.extendFromStream(str, logger);

  BOOST_CHECK_EQUAL(calc.actionCount(), 0);

  BOOST_CHECK_EQUAL(logger.actionCount(), 0);

  f = theSet.getFeast(7, 8);

  BOOST_CHECK(f.name.empty());

  f = theSet.getFeast(3, 1);

  BOOST_CHECK_EQUAL(f.name, "Holy Name of Jesus"s);

Secondly, adding new feasts:

  std::string text2(

   R"(<FEASTS>

<FEAST day="11" month="2" name="Vision of Our Lady" grade="GREATER_DOUBLE"/>

<FEAST day="16" month="7" name="Our Lady of Mount Carmel" grade="GREATER_DOUBLE"/>

<FEAST day="5" month="8" name="Our Lady of the Snows" grade="GREATER_DOUBLE"/>

</FEASTS>

)");

  f = theSet.getFeast(11, 2);

  BOOST_CHECK(f.name.empty());

  f = theSet.getFeast(16, 7);

  BOOST_CHECK(f.name.empty());

  f = theSet.getFeast(5, 8);

  BOOST_CHECK(f.name.empty());

  std::istringstream str2(text2);

  theSet.extendFromStream(str2, logger);

  BOOST_CHECK_EQUAL(calc.actionCount(), 3);

  BOOST_CHECK(calc.matchesAction("isSunday():11, 2"s, 0));

  BOOST_CHECK(calc.matchesAction("isSunday():16, 7"s, 1));

  BOOST_CHECK(calc.matchesAction("isSunday():5, 8"s, 2));

  calc.clearActions();

  BOOST_CHECK_EQUAL(logger.actionCount(), 0);

  f = theSet.getFeast(11, 2);

  BOOST_CHECK_EQUAL(f.name, "Vision of Our Lady"s);

  f = theSet.getFeast(16, 7);

  BOOST_CHECK_EQUAL(f.name, "Our Lady of Mount Carmel"s);

  f = theSet.getFeast(5, 8);

  BOOST_CHECK_EQUAL(f.name, "Our Lady of the Snows"s);

The Saint's Day Set does not function on its own: there's a wrapper class which manages the more complicated interactions with the proper of time:

class Kalendar : public IKalendar

{

public:

  explicit Kalendar(const IDateCalculator &inCalculator,

                    std::unique_ptr<ISaintsDaySet> &&inStorage);

  ~Kalendar() override;

  Feast getFeast(const unsigned int inDay,

                 const unsigned int inMonth) const override;


private:

  const IDateCalculator &m_calculator;

  std::unique_ptr<ISaintsDaySet> m_storage;

};

Kalendar::Kalendar(const IDateCalculator &inCalculator, std::unique_ptr<ISaintsDaySet>&& inStorage):

  m_calculator(inCalculator), m_storage(std::move(inStorage))

{

  auto ld = inCalculator.getLadyDay();

  m_storage->add(ld.day, ld.month, "Annunciation", Grades::DOUBLE_FIRST_CLASS);

  if (m_calculator.isSunday(2, 11))

    m_storage->add(3, 11, "All Souls", Grades::PRIVILEGED_FERIA);

  else

    m_storage->add(2, 11, "All Souls", Grades::PRIVILEGED_FERIA);

}

getFeast() returns Sundays as well as actual saint's days from the stored set, plus some special cases around the Easter cycle:

Feast Kalendar::getFeast(const unsigned int inDay,

                         const unsigned int inMonth) const

{

  // Special cases which take priority

  if ((inMonth == 2) || (inMonth == 3))

    {

      auto [feria, sunday] = m_calculator.getFerialName(inDay, inMonth);

      if (sunday == "Quinquagesima")

        {

          if (feria.find("Wednesday") != std::string::npos)

            return Feast{ "Ash Wednesday", Grades::PRIVILEGED_FERIA };

          auto f = m_storage->getFeast(inDay, inMonth);

          if (!f.name.empty() && !m_calculator.isSunday(inDay, inMonth))

            return f;

          if (feria.find("Thursday") != std::string::npos)

            return Feast{ "Thursday after Ash Wednesday", Grades::FERIA };

          else if (feria.find("Friday") != std::string::npos)

            return Feast{ "Friday after Ash Wednesday", Grades::FERIA };

          else if (feria.find("Saturday") != std::string::npos)

            return Feast{ "Saturday after Ash Wednesday", Grades::FERIA };

          else if ((feria.find("Monday") != std::string::npos)

                   || (feria.find("Tuesday") != std::string::npos))

            return Feast{ feria, Grades::FERIA };

        }

    }

  if ((inMonth == 5) || (inMonth == 6)

      || (inMonth == 7)) // Sacred Heart can be early July

    {

      if (m_calculator.isCorpusChristi(inDay, inMonth))

        return Feast{ "Corpus Christi", Grades::DOUBLE_FIRST_CLASS };

      else if (m_calculator.isSacredHeart(inDay, inMonth))

        return Feast{ "Sacred Heart of Jesus", Grades::DOUBLE_FIRST_CLASS };

    }

  auto f = m_storage->getFeast(inDay, inMonth);

  if (f.name.empty())

    {

      if (m_calculator.isSunday(inDay, inMonth))

        {

          auto name = m_calculator.getSundayName(inDay, inMonth);

          if ((name == "Easter") || (name.find("Lent") != std::string::npos)

              || (name == "Laetare Sunday") || (name == "Passion Sunday")

              || (name == "First Sunday in Advent") || (name == "Pentecost")

              || (name == "Trinity Sunday"))

            return Feast{ name, Grades::DOMINICA_FIRST_CLASS };

          else if (name.find("in Advent") != std::string::npos)

            return Feast{ name, Grades::DOMINICA_SECOND_CLASS };

          else if (name == "First Sunday after Easter")

            return Feast{ name, Grades::GREATER_DOUBLE };

          return Feast{ name, Grades::DOMINICA };

        }

      return m_calculator.getOctaveInfo(inDay, inMonth);

    }

  return f;

}


Comments

Popular posts from this blog

Boundaries

State Machines

Considerations on an Optimization