instructure/canvas-lms · error · RruleValidationError

A yearly RRULE must include BYDAY or BYMONTHDAY

Error message

A yearly RRULE must include BYDAY or BYMONTHDAY

What it means

parse_yearly requires a yearly RRULE to disambiguate its recurrence: either BYDAY (nth weekday of a month) or BYMONTHDAY (specific day of month). A bare FREQ=YEARLY rule with neither clause raises RruleValidationError because Canvas cannot generate the occurrences.

Solutions

  1. Add BYMONTHDAY for same-date-each-year recurrence, e.g. BYMONTH=3;BYMONTHDAY=15
  2. Add BYDAY for nth-weekday recurrence, e.g. BYMONTH=11;BYDAY=4TH (fourth Thursday)
  3. Ensure the RRULE serializer always emits a day clause for FREQ=YEARLY rules

Example fix

// before
"FREQ=YEARLY;INTERVAL=1;COUNT=5"
// after
"FREQ=YEARLY;INTERVAL=1;COUNT=5;BYMONTH=3;BYMONTHDAY=15"
Defensive patterns

Strategy: validation

Validate before calling

if rropts["FREQ"] == "YEARLY"
  raise ArgumentError, "yearly RRULE needs BYDAY or BYMONTHDAY" unless rropts.key?("BYDAY") || rropts.key?("BYMONTHDAY")
end

Try / catch

begin
  RruleHelper.rrule_to_natural_language(rropts)
rescue RruleValidationError => e
  render json: { errors: [e.message] }, status: :bad_request
end

Prevention

When it happens

Trigger: Calling rrule_to_natural_language with rropts like {"FREQ"=>"YEARLY", "INTERVAL"=>"1", "COUNT"=>"5"} — no "BYDAY" and no "BYMONTHDAY" key present.

Common situations: Clients assume FREQ=YEARLY alone means 'same date every year' and omit the day clause; hand-built RRULEs dropping optional-looking parts; imports of simple yearly rules from other calendar systems.

Understand the failure class

Background: "missing required argument" and "the following required arguments were not provided": what required-argument errors mean and how to fix them — this error's family across 20 libraries.

Related errors


AI-assisted analysis of instructure/canvas-lms@1c9f0bb801 (2026-09-15). Data as JSON: /api/errors/ea3a51d8c4d110cd. Report an issue: GitHub.

Appendix: source

Thrown at app/helpers/rrule_helper.rb:397

    else
      I18n.t({
               one: "Monthly until %{until}",
               other: "Every %{count} months until %{until}"
             },
             {
               count: interval,
               until: format_date(until_date)
             })
    end
  end

  def parse_yearly(rropts)
    if rropts["BYDAY"]
      parse_yearly_byday(rropts)
    elsif rropts["BYMONTHDAY"]
      parse_yearly_bymonthday(rropts)
    else
      raise RruleValidationError, I18n.t("A yearly RRULE must include BYDAY or BYMONTHDAY")
    end
  end

  def parse_yearly_byday(rropts)
    times = rropts["COUNT"]
    interval = rropts["INTERVAL"].to_i
    until_date = rropts["UNTIL"]
    month = bymonth_to_month(parse_bymonth(rropts["BYMONTH"]))
    by_days = parse_byday(rropts["BYDAY"])
    days_of_week = by_days.pluck(:day_of_week).join(", ")
    occurrence = by_days.first[:occurrence]

    if times
      if [0, 1].include?(occurrence)
        I18n.t({
                 one: "Annually on the first %{days} of %{month}, %{times} times",
                 other: "Every %{count} years on the first %{days} of %{month}, %{times} times"
               },

View on GitHub (pinned to 1c9f0bb801)