schedule.rb 4.18 KB
Newer Older
Zac Wood's avatar
Zac Wood committed
1 2 3
require 'icalendar'
require 'time'

4
# Creates a iCal object given a list of CRNs
5 6 7 8
class Schedule
  def initialize(crns)
    @cal = Icalendar::Calendar.new
    @cal.x_wr_calname = 'GMU Fall 2018'
Zac Wood's avatar
Zac Wood committed
9

10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
    @course_sections = crns.map do |crn|
      CourseSection.find_by crn: crn
    end
    @course_sections.compact!

    load_events
  end

  def to_ical
    @cal.to_ical
  end

  private

  def load_events
    @course_sections.each do |section|
26
      unless section.start_time == "TBA" || section.end_time == "TBA"
Zac Wood's avatar
Zac Wood committed
27
        event = generate_event_from_section(section)
28
        @cal.add_event(event)
29
      end
30 31 32

      if section.days.start_with? "M"
        col_day_makeup = generate_event_after_columbus_day(section)
33
        @cal.add_event(col_day_makeup)
34
      end
Zac Wood's avatar
Zac Wood committed
35 36 37
    end
  end

38
  # Configures a calendar event from a given section
Zac Wood's avatar
Zac Wood committed
39
  # @param section [CourseSection]
Zac Wood's avatar
Zac Wood committed
40 41
  def generate_event_from_section(section)
    event = Icalendar::Event.new
Zac Wood's avatar
Zac Wood committed
42

Zac Wood's avatar
Zac Wood committed
43 44
    event.summary = section.name
    event.description = section.title
Zac Wood's avatar
Zac Wood committed
45
    event.location = section.location
Zac Wood's avatar
Zac Wood committed
46 47
    event.dtstart = Icalendar::Values::DateTime.new(formatted_datetime_str(section.start_date, section.start_time))
    event.dtend = Icalendar::Values::DateTime.new(formatted_datetime_str(section.start_date, section.end_time))
Zac Wood's avatar
Zac Wood committed
48
    event.rrule = Icalendar::Values::Recur.new(recurrence_rule_str(section))
Zac Wood's avatar
Zac Wood committed
49
    event.exdate = exdates_for_section(section)
Zac Wood's avatar
Zac Wood committed
50 51 52 53

    event
  end

54 55 56 57
  # Format a DateTime string based on a given date and time
  # @param date [String]
  # @param time [String]
  # @return [String]
Zac Wood's avatar
Zac Wood committed
58 59 60
  def formatted_datetime_str(date, time)
    formatted_date = date.to_s.tr('-', '')
    formatted_time = Time.parse(time).strftime("%H%M%S")
Zac Wood's avatar
Zac Wood committed
61

Zac Wood's avatar
Zac Wood committed
62 63 64
    "#{formatted_date}T#{formatted_time}"
  end

65
  # Mapping of days as represented by GMU to the iCal standard
Zac Wood's avatar
Zac Wood committed
66 67 68 69 70 71 72 73 74 75
  DAYS = {
    "M" => "MO",
    "T" => "TU",
    "W" => "WE",
    "R" => "TH",
    "F" => "FR",
    "S" => "SA",
    "U" => "SU"
  }.freeze

76 77
  # Generates a recurrence rule string descripting which day the class event
  # should take place on
Zac Wood's avatar
Zac Wood committed
78
  # @param section [CourseSection]
79
  # @return [String]
Zac Wood's avatar
Zac Wood committed
80
  def recurrence_rule_str(section)
Zac Wood's avatar
Zac Wood committed
81 82 83 84 85 86
    days = section.days.split("").map do |day|
      DAYS[day]
    end

    "FREQ=WEEKLY;UNTIL=#{formatted_datetime_str(section.end_date, section.end_time)};BYDAY=#{days.join(',')}"
  end
Zac Wood's avatar
Zac Wood committed
87

88
  # Get all dates that should excluded from the schedule
Zac Wood's avatar
Zac Wood committed
89
  # @param section [CourseSection]
90
  # @return [Array]
Zac Wood's avatar
Zac Wood committed
91
  def exdates_for_section(section)
92
    # Generate exdates for all closures in a semester
Zac Wood's avatar
Zac Wood committed
93
    exdates = Closure.where(semester: section.course.semester).map { |closure|
Zac Wood's avatar
Zac Wood committed
94
      generate_exdate(closure.date.to_formatted_s(:number), section.start_time)
Zac Wood's avatar
Zac Wood committed
95
    }
Zac Wood's avatar
Zac Wood committed
96 97 98

    # Every section's start_date is the first Monday of the semester.
    # So we need to add an exclusion for that day unless the class is held on Mondays
Zac Wood's avatar
Zac Wood committed
99 100 101
    unless section.days.start_with? "M"
      exdates << generate_exdate(
        section.start_date.to_formatted_s(:number),
Zac Wood's avatar
Zac Wood committed
102
        section.start_time
Zac Wood's avatar
Zac Wood committed
103 104
      )
    end
Zac Wood's avatar
Zac Wood committed
105

106 107 108 109 110 111 112 113
    # If the section meets on Tuesdays, add an exdate for the day after columbus day
    if section.days.start_with? "T"
      exdates << generate_exdate(
        Date.new(2018, 10, 9).to_formatted_s(:number),
        section.start_time
      )
    end

Zac Wood's avatar
Zac Wood committed
114
    exdates
Zac Wood's avatar
Zac Wood committed
115 116
  end

117 118 119 120
  # Generate a DataTime to use as an exdate
  # @param date [String]
  # @param time [String]
  # @return [Icalendar::Values::DateTime]
Zac Wood's avatar
Zac Wood committed
121
  def generate_exdate(date, time)
122
    # format the time for use in a DateTime
Zac Wood's avatar
Zac Wood committed
123 124 125
    formatted_time = Time.parse(time).strftime("%H%M%S")
    Icalendar::Values::DateTime.new("#{date}T#{formatted_time}")
  end
126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141

  # Configures a calendar event for the day after columbus day
  # @param section [CourseSection]
  def generate_event_after_columbus_day(section)
    event = Icalendar::Event.new

    event.summary = section.name + " (Columbus Day makeup)"
    event.description = section.title + " (Columbus Day makeup)"
    event.location = section.location

    after_columbus_day = Date.new 2018, 10, 9
    event.dtstart = Icalendar::Values::DateTime.new(formatted_datetime_str(after_columbus_day, section.start_time))
    event.dtend = Icalendar::Values::DateTime.new(formatted_datetime_str(after_columbus_day, section.end_time))

    event
  end
142
end