{"record":{"id":"cd8e04506a4193b2","repo":"instructure/canvas-lms","slug":"invalid-bymonthday-bymonthday","errorCode":null,"errorMessage":"Invalid BYMONTHDAY '%{bymonthday}'","messagePattern":"Invalid BYMONTHDAY '%(.+?)'","errorType":"validation","errorClass":"RruleValidationError","httpStatus":null,"severity":"error","filePath":"app/helpers/rrule_helper.rb","lineNumber":143,"sourceCode":"        day_of_week: DAYS_OF_WEEK[match[2]]\n      }\n    end\n  end\n\n  def parse_bymonth(bymonth)\n    month = bymonth.to_i\n    raise RruleValidationError, I18n.t(\"Invalid BYMONTH '%{bymonth}'\", bymonth:) unless month.between?(1, 12)\n\n    month\n  end\n\n  def parse_bymonthday(bymonthday, month)\n    raise RruleValidationError, I18n.t(\"Unsupported BYMONTHDAY, only a single day is permitted.\") unless bymonthday.split(\",\").length == 1\n\n    monthday = bymonthday.to_i\n\n    # not validating if we're in a leap year\n    raise RruleValidationError, I18n.t(\"Invalid BYMONTHDAY '%{bymonthday}'\", bymonthday:) unless monthday.between?(1, DAYS_IN_MONTH[month])\n\n    monthday\n  end\n\n  def format_date(date_str)\n    date = date_str.split(\"T\")[0]\n    year = date[0, 4].to_i\n    month = date[4, 2].to_i\n    day = date[6, 2].to_i\n\n    I18n.l(Date.new(year, month, day), format: :medium)\n  end\n\n  def format_month_day(month, day)\n    # 2024 is a leap year, and can handle formatting 2/29\n    I18n.l(Date.new(2024, month, day), format: :short)\n  end\n","sourceCodeStart":125,"sourceCodeEnd":161,"githubUrl":"https://github.com/instructure/canvas-lms/blob/1c9f0bb8013ed69c4f2efe11fd483025469b7e6c/app/helpers/rrule_helper.rb#L125-L161","documentation":"After the single-day check, parse_bymonthday converts BYMONTHDAY to an integer and requires it to fall between 1 and the number of days in the given BYMONTH (DAYS_IN_MONTH lookup, leap years not considered here). Values like 0, negative numbers, non-numeric strings (to_i => 0), or 30 in February raise RruleValidationError.","triggerScenarios":"Calling rrule_to_natural_language with a yearly RRULE where BYMONTHDAY is 0, -1, \"x\", 31 with BYMONTH=4 (April has 30), or 30 with BYMONTH=2.","commonSituations":"Clients modeling 'last day of month' with negative values (-1), which RFC-5545 allows but Canvas rejects; picking day 29/30/31 for February; typos like day 0 from off-by-one loops.","solutions":["Use BYMONTHDAY between 1 and the number of days in the chosen BYMONTH","For February, cap at 29 (Canvas does not special-case leap years here)","Model 'last day of month' semantics differently (e.g. BYDAY=-1SU-style or fixed day 28) since negative monthdays are invalid in this validation"],"exampleFix":"// before\n\"FREQ=YEARLY;INTERVAL=1;BYMONTH=2;BYMONTHDAY=30\"\n// after\n\"FREQ=YEARLY;INTERVAL=1;BYMONTH=2;BYMONTHDAY=28\"","handlingStrategy":"validation","validationCode":"days = { 1=>31, 2=>29, 3=>31, 4=>30, 5=>31, 6=>30, 7=>31, 8=>31, 9=>30, 10=>31, 11=>30, 12=>31 }\nraise ArgumentError, \"BYMONTHDAY out of range\" unless bymonthday.to_i.between?(1, days[month])","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":["Cap February at 29 (leap-year edge cases are not validated)","Do not use negative BYMONTHDAY values; Canvas rejects them","Validate day against the selected month in the UI before submitting"],"tags":["rrule","validation","bymonthday","calendar"],"backgroundTag":"value-out-of-range","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"}