{"record":{"id":"ea3a51d8c4d110cd","repo":"instructure/canvas-lms","slug":"a-yearly-rrule-must-include-byday-or-bymonthday","errorCode":null,"errorMessage":"A yearly RRULE must include BYDAY or BYMONTHDAY","messagePattern":"A yearly RRULE must include BYDAY or BYMONTHDAY","errorType":"validation","errorClass":"RruleValidationError","httpStatus":null,"severity":"error","filePath":"app/helpers/rrule_helper.rb","lineNumber":397,"sourceCode":"    else\n      I18n.t({\n               one: \"Monthly until %{until}\",\n               other: \"Every %{count} months until %{until}\"\n             },\n             {\n               count: interval,\n               until: format_date(until_date)\n             })\n    end\n  end\n\n  def parse_yearly(rropts)\n    if rropts[\"BYDAY\"]\n      parse_yearly_byday(rropts)\n    elsif rropts[\"BYMONTHDAY\"]\n      parse_yearly_bymonthday(rropts)\n    else\n      raise RruleValidationError, I18n.t(\"A yearly RRULE must include BYDAY or BYMONTHDAY\")\n    end\n  end\n\n  def parse_yearly_byday(rropts)\n    times = rropts[\"COUNT\"]\n    interval = rropts[\"INTERVAL\"].to_i\n    until_date = rropts[\"UNTIL\"]\n    month = bymonth_to_month(parse_bymonth(rropts[\"BYMONTH\"]))\n    by_days = parse_byday(rropts[\"BYDAY\"])\n    days_of_week = by_days.pluck(:day_of_week).join(\", \")\n    occurrence = by_days.first[:occurrence]\n\n    if times\n      if [0, 1].include?(occurrence)\n        I18n.t({\n                 one: \"Annually on the first %{days} of %{month}, %{times} times\",\n                 other: \"Every %{count} years on the first %{days} of %{month}, %{times} times\"\n               },","sourceCodeStart":379,"sourceCodeEnd":415,"githubUrl":"https://github.com/instructure/canvas-lms/blob/1c9f0bb8013ed69c4f2efe11fd483025469b7e6c/app/helpers/rrule_helper.rb#L379-L415","documentation":"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.","triggerScenarios":"Calling rrule_to_natural_language with rropts like {\"FREQ\"=>\"YEARLY\", \"INTERVAL\"=>\"1\", \"COUNT\"=>\"5\"} — no \"BYDAY\" and no \"BYMONTHDAY\" key present.","commonSituations":"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.","solutions":["Add BYMONTHDAY for same-date-each-year recurrence, e.g. BYMONTH=3;BYMONTHDAY=15","Add BYDAY for nth-weekday recurrence, e.g. BYMONTH=11;BYDAY=4TH (fourth Thursday)","Ensure the RRULE serializer always emits a day clause for FREQ=YEARLY rules"],"exampleFix":"// before\n\"FREQ=YEARLY;INTERVAL=1;COUNT=5\"\n// after\n\"FREQ=YEARLY;INTERVAL=1;COUNT=5;BYMONTH=3;BYMONTHDAY=15\"","handlingStrategy":"validation","validationCode":"if rropts[\"FREQ\"] == \"YEARLY\"\n  raise ArgumentError, \"yearly RRULE needs BYDAY or BYMONTHDAY\" unless rropts.key?(\"BYDAY\") || rropts.key?(\"BYMONTHDAY\")\nend","typeGuard":null,"tryCatchPattern":"begin\n  RruleHelper.rrule_to_natural_language(rropts)\nrescue RruleValidationError => e\n  render json: { errors: [e.message] }, status: :bad_request\nend","preventionTips":["Always pair FREQ=YEARLY with BYMONTH+BYMONTHDAY or BYDAY","Emit an explicit day clause from your RRULE serializer for yearly rules","Catch RruleValidationError in yearly flows and surface a client-facing message"],"tags":["rrule","validation","yearly","recurrence"],"backgroundTag":"missing-required-argument","analyzedSha":"1c9f0bb8013ed69c4f2efe11fd483025469b7e6c","analyzedAt":"2026-09-15T20:33:18.891Z","contentChangedAt":"2026-09-15T20:33:18.891Z","schemaVersion":2},"datasetVersion":"2026-09-23T02:17:17.105Z"}