class Mhc::PropertyValue::RecurrenceCondition

Public Class Methods

new() click to toggle source
# File lib/mhc/property_value/recurrence_condition.rb, line 33
def initialize
  @cond_mon, @cond_ord, @cond_wek, @cond_num = [], [], [], []
end

Public Instance Methods

cond_mon() click to toggle source
# File lib/mhc/property_value/recurrence_condition.rb, line 28
def cond_mon; return @cond_mon; end
cond_num() click to toggle source
# File lib/mhc/property_value/recurrence_condition.rb, line 31
def cond_num; return @cond_num; end
cond_ord() click to toggle source
# File lib/mhc/property_value/recurrence_condition.rb, line 29
def cond_ord; return @cond_ord; end
cond_wek() click to toggle source
# File lib/mhc/property_value/recurrence_condition.rb, line 30
def cond_wek; return @cond_wek; end
daily?() click to toggle source
# File lib/mhc/property_value/recurrence_condition.rb, line 74
def daily?
  false
end
empty?() click to toggle source
# File lib/mhc/property_value/recurrence_condition.rb, line 94
def empty?
  [@cond_mon, @cond_ord, @cond_wek, @cond_num].all?{|cond| cond.empty?}
end
frequency() click to toggle source
# File lib/mhc/property_value/recurrence_condition.rb, line 66
def frequency
  return :none    if empty?
  return :daily   if daily?
  return :weekly  if weekly?
  return :monthly if monthly?
  return :yearly  if yearly?
end
monthly?() click to toggle source
# File lib/mhc/property_value/recurrence_condition.rb, line 82
def monthly?
  !yearly? && (!cond_num.empty? || !cond_ord.empty?)
end
parse(string) click to toggle source
# File lib/mhc/property_value/recurrence_condition.rb, line 37
def parse(string)
  o = self
  string.split.grep(MON_REGEXP) {|mon| o.cond_mon << MON_L2V[mon.capitalize]}
  string.split.grep(ORD_REGEXP) {|ord| o.cond_ord << ORD_L2V[ord.capitalize]}
  string.split.grep(WEK_REGEXP) {|wek| o.cond_wek << WEK_L2V[wek.capitalize]}
  string.split.grep(NUM_REGEXP) {|num| o.cond_num << num.to_i}
  return o
end
set_from_ics(rrule, dtstart) click to toggle source
# File lib/mhc/property_value/recurrence_condition.rb, line 161
def set_from_ics(rrule, dtstart)
  if (errno = validate_rrule(rrule)) != true
    raise "Unsupported RRULE string (errno=#{errno}): #{rrule}"
  end

  ################
  ## BYMONTH (cond_mon)
  cond_mon = []
  if rrule =~ /BYMONTH=([^;]+)/
    $1.split(",").each do |mon|
      cond_mon << mon.to_i
    end
  end

  ################
  ## BYDAY (cond_ord, cond_wek)
  cond_ord = []
  cond_wek = []
  week = {}
  if rrule =~ /BYDAY=([^;]+)/
    $1.scan(/(1|2|3|4|-1)?(MO|TU|WE|TH|FR|SA|SU)/).each do |o,w|
      week[w] ||= []
      week[w] << o.to_i # unpefixed week is replaced as 0
    end

    # Every week should have the same number-prefix set:
    return 9 unless week.values.all?{|orders| orders.sort == week.values.first.sort}

    order = week.values.first.sort
    #     * Number-prefixed week cannot coexist with unprefixed week
    #       WE,SU  is OK => Wed Sun
    #       WE,3SU is NG
    return 10 if order.length > 1 and order.member?(0) # 0 means non-numberd prefix

    order.delete(0)
    cond_ord = order

    week.each do |w, o|
      cond_wek << WEK_V2I.invert[w]
    end
  end

  ################
  ## BYMONTHDAY (cond_num)
  cond_num = []
  if rrule =~ /BYMONTHDAY=([^;]+)/i
    $1.split(",").each do |n|
      cond_num << n.to_i
    end
  end

  ################
  # Special cases

  interval = (rrule =~ /INTERVAL=(\d+)/i) ? $1.to_i : 1

  # special case of yearly: repeat with 12 months interval
  # BYMONTH should be taken from DTSTART
  if interval == 12 and rrule =~ /FREQ=MONTHLY/i and cond_mon.empty?
    cond_mon << dtstart.month
  end

  # if RRULE has only FREQ=YEARLY phrase,
  # BYMONTH and BYMONTHDAY should be taken from DTSTART
  #
  if rrule =~ /FREQ=YEARLY/i
    cond_mon << dtstart.month if cond_mon.empty?
    cond_num << dtstart.day   if cond_num.empty? and cond_wek.empty?
  end

  @cond_mon, @cond_ord, @cond_wek, @cond_num = cond_mon, cond_ord, cond_wek, cond_num
  return self
end
to_ics(dtstart = nil, until_date = nil) click to toggle source
# File lib/mhc/property_value/recurrence_condition.rb, line 243
def to_ics(dtstart = nil, until_date = nil)
  return nil unless valid?

  ord_wek = (cond_ord.empty? ? [""] : cond_ord).product(cond_wek)
  day = ord_wek.map {|o,w| o.to_s + WEK_V2I[w] }.join(',')

  if until_date
    if dtstart.respond_to?(:hour)
      tz = TZInfo::Timezone.get(ENV["MHC_TZID"] || 'UTC')
      localtime = Mhc::PropertyValue::Time.new.parse(dtstart.strftime("%H:%M")).to_datetime(until_date).to_time
      until_str = tz.local_to_utc(localtime).strftime("%Y%m%dT%H%M%SZ")
      # puts "until_str local (tz=#{tz.name}) : #{localtime.strftime("%Y%m%dT%H%M%S")} utc: #{until_str}"
    else
      until_str = until_date.strftime("%Y%m%d")
    end
  end

  ics = "FREQ=#{frequency.to_s.upcase};INTERVAL=1;WKST=MO"

  ics += ";BYMONTH=#{cond_mon.join(',')}"    unless cond_mon.empty?
  ics += ";BYDAY=#{day}"                     unless day.empty?
  ics += ";BYMONTHDAY=#{cond_num.join(',')}" unless cond_num.empty?
  ics += ";UNTIL=#{until_str}" unless until_date.nil?

  return ics
end
to_mhc_string() click to toggle source
# File lib/mhc/property_value/recurrence_condition.rb, line 235
def to_mhc_string
  return (cond_mon.map{|mon| MON_V2L[mon]} +
          cond_ord.map{|ord| ORD_V2L[ord]} +
          cond_wek.map{|wek| WEK_V2L[wek]} +
          cond_num.map{|num| num.to_s}
          ).join(" ")
end
valid?() click to toggle source
# File lib/mhc/property_value/recurrence_condition.rb, line 90
def valid?
  frequency != :none
end
validate_rrule(rrule) click to toggle source

convert RRULE to X-SC-Cond:

Due to the over-killing complexity of iCalendar (RFC5545) format, converting RRULE to X-SC-* format has some restrictions:

  • Not allowed elements:

    • BYSECOND

    • BYMINUTE

    • BYHOUR

    • COUNT

    • BYYEARDAY (-366 to 366)

    • BYWEEKNO (-53 to 53)

    • BYSETPOS (-366 to 366)

    • Recurrence-ID (not part of RRULE)

  • Restricted elements:

    • INTERVAL:

      • it should be 1

    • BYMONTHDAY:

      • it should be (1..31)

    • WKST:

      • it should be MO

    • FREQ:

      • should be one of WEEKLY, MONTHLY, YEARLY

      • should be MONTHLY if BYDAY has (1|2|3|4|-1)

      • should be WEEKLY if BYDAY does not have (1|2|3|4|-1)

    • BYDAY:

      • should be a list of (1|2|3|4|-1)?(MO|TU|WE|TH|FR|SA|SU)

      • Every week should have the same number-prefix set: WE,SU is OK => Wed Sun 3WE,3SU is OK => 3rd Wed Sun 2WE,3WE,2SU,3SU is OK => 2nd 3rd Sun Wed 3WE,2SU is NG 3WE,SU is NG

  • Fully converted elements:

    • UNTIL

      • YYYYMMDD should goes to X-SC-Duration: -YYYYMMDD

    • BYMONTH

      • (1..12)* => (Jan|Feb|Mar|Jul|Aug|Sep|Oct|Nov|Dec)*

# File lib/mhc/property_value/recurrence_condition.rb, line 147
def validate_rrule(rrule)
  interval = (rrule =~ /INTERVAL=(\d+)/i) ? $1.to_i : 1
  return true if rrule.to_s == ""
  return 1 if rrule =~ /(BYSECOND|BYMINUTE|BYHOUR|COUNT|BYYEARDAY|BYWEEKNO|BYSETPOS)/i
  return 2 unless (rrule =~ /FREQ=MONTHLY/i and interval == 12) || interval == 1
  return 3 if rrule =~ /BYMONTHDAY=([^;]+)/i and $1.split(",").map(&:to_i).any?{|i| i < 1 or i > 31}
  return 4 if rrule =~ /WKST=([^;]+)/i and $1 !~ /MO/
  return 5 if rrule =~ /FREQ=([^;]+)/i   and $1 !~ /WEEKLY|MONTHLY|YEARLY/i
  return 6 if rrule =~ /BYDAY=([^;]+)/i  and $1 =~ /\d/ and rrule !~ /FREQ=MONTHLY/i
  return 7 if rrule =~ /BYDAY=([^;]+)/i  and $1 !~ /\d/ and rrule !~ /FREQ=WEEKLY/i
  return 8 if rrule =~ /BYDAY=([^;]+)/i  and $1 !~ /((1|2|3|4|-1)?(MO|TU|WE|TH|FR|SA|SU))+/i
  return true
end
weekly?() click to toggle source
# File lib/mhc/property_value/recurrence_condition.rb, line 78
def weekly?
  !yearly? && !monthly? && !cond_wek.empty?
end
yearly?() click to toggle source
# File lib/mhc/property_value/recurrence_condition.rb, line 86
def yearly?
  !cond_mon.empty?
end