initial port of handler methods
authorChuck Hagenbuch <chuck@horde.org>
Thu, 29 Jan 2009 02:56:02 +0000 (21:56 -0500)
committerChuck Hagenbuch <chuck@horde.org>
Thu, 29 Jan 2009 02:56:02 +0000 (21:56 -0500)
framework/Date_Parser/lib/Horde/Date/Parser/Locale/Base.php

index aae5818..c6537af 100644 (file)
@@ -1,6 +1,8 @@
 <?php
 class Horde_Date_Parser_Locale_Base
 {
+    public $definitions = array();
+
     /**
     # Parses a string containing a natural language date or time. If the parser
     # can find a date or time, either a Time or Chronic::Span will be returned
@@ -171,427 +173,474 @@ class Horde_Date_Parser_Locale_Base
         }
     }
 
-      def definitions #:nodoc:
-        @definitions ||=
-      {:time => [Handler.new([:repeater_time, :repeater_day_portion?], nil)],
-
-       :date => [Handler.new([:repeater_day_name, :repeater_month_name, :scalar_day, :repeater_time, :time_zone, :scalar_year], :handle_rdn_rmn_sd_t_tz_sy),
-                 Handler.new([:repeater_month_name, :scalar_day, :scalar_year], :handle_rmn_sd_sy),
-                 Handler.new([:repeater_month_name, :scalar_day, :scalar_year, :separator_at?, 'time?'], :handle_rmn_sd_sy),
-                 Handler.new([:repeater_month_name, :scalar_day, :separator_at?, 'time?'], :handle_rmn_sd),
-                 Handler.new([:repeater_month_name, :ordinal_day, :separator_at?, 'time?'], :handle_rmn_od),
-                 Handler.new([:repeater_month_name, :scalar_year], :handle_rmn_sy),
-                 Handler.new([:scalar_day, :repeater_month_name, :scalar_year, :separator_at?, 'time?'], :handle_sd_rmn_sy),
-                 Handler.new([:scalar_month, :separator_slash_or_dash, :scalar_day, :separator_slash_or_dash, :scalar_year, :separator_at?, 'time?'], :handle_sm_sd_sy),
-                 Handler.new([:scalar_day, :separator_slash_or_dash, :scalar_month, :separator_slash_or_dash, :scalar_year, :separator_at?, 'time?'], :handle_sd_sm_sy),
-                 Handler.new([:scalar_year, :separator_slash_or_dash, :scalar_month, :separator_slash_or_dash, :scalar_day, :separator_at?, 'time?'], :handle_sy_sm_sd),
-                 Handler.new([:scalar_month, :separator_slash_or_dash, :scalar_year], :handle_sm_sy)],
-
-       # tonight at 7pm
-       :anchor => [Handler.new([:grabber?, :repeater, :separator_at?, :repeater?, :repeater?], :handle_r),
-                   Handler.new([:grabber?, :repeater, :repeater, :separator_at?, :repeater?, :repeater?], :handle_r),
-                   Handler.new([:repeater, :grabber, :repeater], :handle_r_g_r)],
-
-       # 3 weeks from now, in 2 months
-       :arrow => [Handler.new([:scalar, :repeater, :pointer], :handle_s_r_p),
-                  Handler.new([:pointer, :scalar, :repeater], :handle_p_s_r),
-                  Handler.new([:scalar, :repeater, :pointer, 'anchor'], :handle_s_r_p_a)],
-
-       # 3rd week in march
-       :narrow => [Handler.new([:ordinal, :repeater, :separator_in, :repeater], :handle_o_r_s_r),
-                   Handler.new([:ordinal, :repeater, :grabber, :repeater], :handle_o_r_g_r)]
-      }
-    end
-
-    def tokens_to_span(tokens, options) #:nodoc:
-      # maybe it's a specific date
-
-      self.definitions[:date].each do |handler|
-        if handler.match(tokens, self.definitions)
-          puts "-date" if Chronic.debug
-          good_tokens = tokens.select { |o| !o.get_tag Separator }
-          return self.send(handler.handler_method, good_tokens, options)
-        end
-      end
-
-      # I guess it's not a specific date, maybe it's just an anchor
-
-      self.definitions[:anchor].each do |handler|
-        if handler.match(tokens, self.definitions)
-          puts "-anchor" if Chronic.debug
-          good_tokens = tokens.select { |o| !o.get_tag Separator }
-          return self.send(handler.handler_method, good_tokens, options)
-        end
-      end
-
-      # not an anchor, perhaps it's an arrow
-
-      self.definitions[:arrow].each do |handler|
-        if handler.match(tokens, self.definitions)
-          puts "-arrow" if Chronic.debug
-          good_tokens = tokens.reject { |o| o.get_tag(SeparatorAt) || o.get_tag(SeparatorSlashOrDash) || o.get_tag(SeparatorComma) }
-          return self.send(handler.handler_method, good_tokens, options)
-        end
-      end
-
-      # not an arrow, let's hope it's a narrow
-
-      self.definitions[:narrow].each do |handler|
-        if handler.match(tokens, self.definitions)
-          puts "-narrow" if Chronic.debug
-          #good_tokens = tokens.select { |o| !o.get_tag Separator }
-          return self.send(handler.handler_method, tokens, options)
-        end
-      end
-
-      # I guess you're out of luck!
-      puts "-none" if Chronic.debug
-      return nil
-    end
-
-    #--------------
-
-    def day_or_time(day_start, time_tokens, options)
-      outer_span = Span.new(day_start, day_start + (24 * 60 * 60))
-
-      if !time_tokens.empty?
-        @now = outer_span.begin
-        time = get_anchor(dealias_and_disambiguate_times(time_tokens, options), options)
-        return time
-      else
-        return outer_span
-      end
-    end
-
-    #--------------
-
-    def handle_m_d(month, day, time_tokens, options) #:nodoc:
-      month.start = @now
-      span = month.this(options[:context])
-
-      day_start = Time.local(span.begin.year, span.begin.month, day)
-
-      day_or_time(day_start, time_tokens, options)
-    end
-
-    def handle_rmn_sd(tokens, options) #:nodoc:
-      handle_m_d(tokens[0].get_tag(RepeaterMonthName), tokens[1].get_tag(ScalarDay).type, tokens[2..tokens.size], options)
-    end
-
-    def handle_rmn_od(tokens, options) #:nodoc:
-      handle_m_d(tokens[0].get_tag(RepeaterMonthName), tokens[1].get_tag(OrdinalDay).type, tokens[2..tokens.size], options)
-    end
-
-    def handle_rmn_sy(tokens, options) #:nodoc:
-      month = tokens[0].get_tag(RepeaterMonthName).index
-      year = tokens[1].get_tag(ScalarYear).type
-
-      if month == 12
-        next_month_year = year + 1
-        next_month_month = 1
-      else
-        next_month_year = year
-        next_month_month = month + 1
-      end
-
-      begin
-        Span.new(Time.local(year, month), Time.local(next_month_year, next_month_month))
-      rescue ArgumentError
-        nil
-      end
-    end
-
-    def handle_rdn_rmn_sd_t_tz_sy(tokens, options) #:nodoc:
-      month = tokens[1].get_tag(RepeaterMonthName).index
-      day = tokens[2].get_tag(ScalarDay).type
-      year = tokens[5].get_tag(ScalarYear).type
-
-      begin
-        day_start = Time.local(year, month, day)
-        day_or_time(day_start, [tokens[3]], options)
-      rescue ArgumentError
-        nil
-      end
-    end
-
-    def handle_rmn_sd_sy(tokens, options) #:nodoc:
-      month = tokens[0].get_tag(RepeaterMonthName).index
-      day = tokens[1].get_tag(ScalarDay).type
-      year = tokens[2].get_tag(ScalarYear).type
-
-      time_tokens = tokens.last(tokens.size - 3)
-
-      begin
-        day_start = Time.local(year, month, day)
-        day_or_time(day_start, time_tokens, options)
-      rescue ArgumentError
-        nil
-      end
-    end
-
-    def handle_sd_rmn_sy(tokens, options) #:nodoc:
-      new_tokens = [tokens[1], tokens[0], tokens[2]]
-      time_tokens = tokens.last(tokens.size - 3)
-      self.handle_rmn_sd_sy(new_tokens + time_tokens, options)
-    end
-
-    def handle_sm_sd_sy(tokens, options) #:nodoc:
-      month = tokens[0].get_tag(ScalarMonth).type
-      day = tokens[1].get_tag(ScalarDay).type
-      year = tokens[2].get_tag(ScalarYear).type
-
-      time_tokens = tokens.last(tokens.size - 3)
-
-      begin
-        day_start = Time.local(year, month, day) #:nodoc:
-        day_or_time(day_start, time_tokens, options)
-      rescue ArgumentError
-        nil
-      end
-    end
-
-    def handle_sd_sm_sy(tokens, options) #:nodoc:
-      new_tokens = [tokens[1], tokens[0], tokens[2]]
-      time_tokens = tokens.last(tokens.size - 3)
-      self.handle_sm_sd_sy(new_tokens + time_tokens, options)
-    end
-
-    def handle_sy_sm_sd(tokens, options) #:nodoc:
-      new_tokens = [tokens[1], tokens[2], tokens[0]]
-      time_tokens = tokens.last(tokens.size - 3)
-      self.handle_sm_sd_sy(new_tokens + time_tokens, options)
-    end
-
-    def handle_sm_sy(tokens, options) #:nodoc:
-      month = tokens[0].get_tag(ScalarMonth).type
-      year = tokens[1].get_tag(ScalarYear).type
-
-      if month == 12
-        next_month_year = year + 1
-        next_month_month = 1
-      else
-        next_month_year = year
-        next_month_month = month + 1
-      end
-
-      begin
-        Span.new(Time.local(year, month), Time.local(next_month_year, next_month_month))
-      rescue ArgumentError
-        nil
-      end
-    end
-
-    # anchors
-
-    def handle_r(tokens, options) #:nodoc:
-      dd_tokens = dealias_and_disambiguate_times(tokens, options)
-      self.get_anchor(dd_tokens, options)
-    end
-
-    def handle_r_g_r(tokens, options) #:nodoc:
-      new_tokens = [tokens[1], tokens[0], tokens[2]]
-      self.handle_r(new_tokens, options)
-    end
-
-    # arrows
-
-    def handle_srp(tokens, span, options) #:nodoc:
-      distance = tokens[0].get_tag(Scalar).type
-      repeater = tokens[1].get_tag(Repeater)
-      pointer = tokens[2].get_tag(Pointer).type
-
-      repeater.offset(span, distance, pointer)
-    end
-
-    def handle_s_r_p(tokens, options) #:nodoc:
-      repeater = tokens[1].get_tag(Repeater)
-
-      # span =
-      # case true
-      # when [RepeaterYear, RepeaterSeason, RepeaterSeasonName, RepeaterMonth, RepeaterMonthName, RepeaterFortnight, RepeaterWeek].include?(repeater.class)
-      #   self.parse("this hour", :guess => false, :now => @now)
-      # when [RepeaterWeekend, RepeaterDay, RepeaterDayName, RepeaterDayPortion, RepeaterHour].include?(repeater.class)
-      #   self.parse("this minute", :guess => false, :now => @now)
-      # when [RepeaterMinute, RepeaterSecond].include?(repeater.class)
-      #   self.parse("this second", :guess => false, :now => @now)
-      # else
-      #   raise(ChronicPain, "Invalid repeater: #{repeater.class}")
-      # end
-
-      span = self.parse("this second", :guess => false, :now => @now)
-
-      self.handle_srp(tokens, span, options)
-    end
-
-    def handle_p_s_r(tokens, options) #:nodoc:
-      new_tokens = [tokens[1], tokens[2], tokens[0]]
-      self.handle_s_r_p(new_tokens, options)
-    end
-
-    def handle_s_r_p_a(tokens, options) #:nodoc:
-      anchor_span = get_anchor(tokens[3..tokens.size - 1], options)
-      self.handle_srp(tokens, anchor_span, options)
-    end
-
-    # narrows
-
-    def handle_orr(tokens, outer_span, options) #:nodoc:
-      repeater = tokens[1].get_tag(Repeater)
-      repeater.start = outer_span.begin - 1
-      ordinal = tokens[0].get_tag(Ordinal).type
-      span = nil
-      ordinal.times do
-        span = repeater.next(:future)
-        if span.begin > outer_span.end
-          span = nil
-          break
-        end
-      end
-      span
-    end
-
-    def handle_o_r_s_r(tokens, options) #:nodoc:
-      outer_span = get_anchor([tokens[3]], options)
-      handle_orr(tokens[0..1], outer_span, options)
-    end
-
-    def handle_o_r_g_r(tokens, options) #:nodoc:
-      outer_span = get_anchor(tokens[2..3], options)
-      handle_orr(tokens[0..1], outer_span, options)
-    end
-
-    # support methods
-
-    def get_anchor(tokens, options) #:nodoc:
-      grabber = Grabber.new(:this)
-      pointer = :future
-
-      repeaters = self.get_repeaters(tokens)
-      repeaters.size.times { tokens.pop }
-
-      if tokens.first && tokens.first.get_tag(Grabber)
-        grabber = tokens.first.get_tag(Grabber)
-        tokens.pop
-      end
-
-      head = repeaters.shift
-      head.start = @now
-
-      case grabber.type
-        when :last
-          outer_span = head.next(:past)
-        when :this
-          if repeaters.size > 0
-            outer_span = head.this(:none)
-          else
-            outer_span = head.this(options[:context])
-          end
-        when :next
-          outer_span = head.next(:future)
-        else raise(ChronicPain, "Invalid grabber")
-      end
-
-      puts "--#{outer_span}" if Chronic.debug
-      anchor = find_within(repeaters, outer_span, pointer)
-    end
-
-    def get_repeaters(tokens) #:nodoc:
-      repeaters = []
-          tokens.each do |token|
-            if t = token.get_tag(Repeater)
-          repeaters << t
-        end
-      end
-      repeaters.sort.reverse
-    end
-
-    # Recursively finds repeaters within other repeaters.
-    # Returns a Span representing the innermost time span
-    # or nil if no repeater union could be found
-    def find_within(tags, span, pointer) #:nodoc:
-      puts "--#{span}" if Chronic.debug
-      return span if tags.empty?
-
-      head, *rest = tags
-      head.start = pointer == :future ? span.begin : span.end
-      h = head.this(:none)
-
-      if span.include?(h.begin) || span.include?(h.end)
-        return find_within(rest, h, pointer)
-      else
-        return nil
-      end
-    end
-
-    def dealias_and_disambiguate_times(tokens, options) #:nodoc:
-      # handle aliases of am/pm
-      # 5:00 in the morning -> 5:00 am
-      # 7:00 in the evening -> 7:00 pm
-
-      day_portion_index = nil
-      tokens.each_with_index do |t, i|
-        if t.get_tag(RepeaterDayPortion)
-          day_portion_index = i
-          break
-        end
-      end
-
-      time_index = nil
-      tokens.each_with_index do |t, i|
-        if t.get_tag(RepeaterTime)
-          time_index = i
-          break
-        end
-      end
-
-      if (day_portion_index && time_index)
-        t1 = tokens[day_portion_index]
-        t1tag = t1.get_tag(RepeaterDayPortion)
-
-        if [:morning].include?(t1tag.type)
-          puts '--morning->am' if Chronic.debug
-          t1.untag(RepeaterDayPortion)
-          t1.tag(RepeaterDayPortion.new(:am))
-        elsif [:afternoon, :evening, :night].include?(t1tag.type)
-          puts "--#{t1tag.type}->pm" if Chronic.debug
-          t1.untag(RepeaterDayPortion)
-          t1.tag(RepeaterDayPortion.new(:pm))
-        end
-      end
-
-      # tokens.each_with_index do |t0, i|
-      #   t1 = tokens[i + 1]
-      #   if t1 && (t1tag = t1.get_tag(RepeaterDayPortion)) && t0.get_tag(RepeaterTime)
-      #     if [:morning].include?(t1tag.type)
-      #       puts '--morning->am' if Chronic.debug
-      #       t1.untag(RepeaterDayPortion)
-      #       t1.tag(RepeaterDayPortion.new(:am))
-      #     elsif [:afternoon, :evening, :night].include?(t1tag.type)
-      #       puts "--#{t1tag.type}->pm" if Chronic.debug
-      #       t1.untag(RepeaterDayPortion)
-      #       t1.tag(RepeaterDayPortion.new(:pm))
-      #     end
-      #   end
-      # end
-
-      # handle ambiguous times if :ambiguous_time_range is specified
-      if options[:ambiguous_time_range] != :none
-        ttokens = []
-        tokens.each_with_index do |t0, i|
-          ttokens << t0
-          t1 = tokens[i + 1]
-          if t0.get_tag(RepeaterTime) && t0.get_tag(RepeaterTime).type.ambiguous? && (!t1 || !t1.get_tag(RepeaterDayPortion))
-            distoken = Token.new('disambiguator')
-            distoken.tag(RepeaterDayPortion.new(options[:ambiguous_time_range]))
-            ttokens << distoken
-          end
-        end
-        tokens = ttokens
-      end
-
-      tokens
-    end
-
-  end
+    public function initDefinitions()
+    {
+        if ($this->definitions) { return; }
+
+        $this->definitions = array(
+            'time' => array(
+                new Horde_Date_Parser_Handler(array('repeater_time', 'repeater_day_portion?'), null),
+            ),
+
+            'date' => array(
+                new Horde_Date_Parser_Handler(array('repeater_day_name', 'repeater_month_name', 'scalar_day', 'repeater_time', 'time_zone', 'scalar_year'), 'handle_rdn_rmn_sd_t_tz_sy'),
+                new Horde_Date_Parser_Handler(array('repeater_month_name', 'scalar_day', 'scalar_year'), 'handle_rmn_sd_sy'),
+                new Horde_Date_Parser_Handler(array('repeater_month_name', 'scalar_day', 'scalar_year', 'separator_at?', 'time?'), 'handle_rmn_sd_sy'),
+                new Horde_Date_Parser_Handler(array('repeater_month_name', 'scalar_day', 'separator_at?', 'time?'), 'handle_rmn_sd'),
+                new Horde_Date_Parser_Handler(array('repeater_month_name', 'ordinal_day', 'separator_at?', 'time?'), 'handle_rmn_od'),
+                new Horde_Date_Parser_Handler(array('repeater_month_name', 'scalar_year'), 'handle_rmn_sy'),
+                new Horde_Date_Parser_Handler(array('scalar_day', 'repeater_month_name', 'scalar_year', 'separator_at?', 'time?'), 'handle_sd_rmn_sy'),
+                new Horde_Date_Parser_Handler(array('scalar_month', 'separator_slash_or_dash', 'scalar_day', 'separator_slash_or_dash', 'scalar_year', 'separator_at?', 'time?'), 'handle_sm_sd_sy'),
+                new Horde_Date_Parser_Handler(array('scalar_day', 'separator_slash_or_dash', 'scalar_month', 'separator_slash_or_dash', 'scalar_year', 'separator_at?', 'time?'), 'handle_sd_sm_sy'),
+                new Horde_Date_Parser_Handler(array('scalar_year', 'separator_slash_or_dash', 'scalar_month', 'separator_slash_or_dash', 'scalar_day', 'separator_at?', 'time?'), 'handle_sy_sm_sd'),
+                new Horde_Date_Parser_Handler(array('scalar_month', 'separator_slash_or_dash', 'scalar_year'), 'handle_sm_sy'),
+            ),
+
+            // tonight at 7pm
+            'anchor' => array(
+                new Horde_Date_Parser_Handler(array('grabber?', 'repeater', 'separator_at?', 'repeater?', 'repeater?'), 'handle_r'),
+                new Horde_Date_Parser_Handler(array('grabber?', 'repeater', 'repeater', 'separator_at?', 'repeater?', 'repeater?'), 'handle_r'),
+                new Horde_Date_Parser_Handler(array('repeater', 'grabber', 'repeater'), 'handle_r_g_r'),
+            ),
+
+            // 3 weeks from now, in 2 months
+            'arrow' => array(
+                new Horde_Date_Parser_Handler(array('scalar', 'repeater', 'pointer'), 'handle_s_r_p'),
+                new Horde_Date_Parser_Handler(array('pointer', 'scalar', 'repeater'), 'handle_p_s_r'),
+                new Horde_Date_Parser_Handler(array('scalar', 'repeater', 'pointer', 'anchor'), 'handle_s_r_p_a'),
+            ),
+
+            // 3rd week in march
+            'narrow' => array(
+                new Horde_Date_Parser_Handler(array('ordinal', 'repeater', 'separator_in', 'repeater'), 'handle_o_r_s_r'),
+                new Horde_Date_Parser_Handler(array('ordinal', 'repeater', 'grabber', 'repeater'), 'handle_o_r_g_r'),
+            ),
+        );
+    }
+
+    public function tokensToSpans($tokens, $options)
+    {
+        $this->initDefinitions();
+
+        // maybe it's a specific date
+        foreach ($this->definitions['date'] as $handler) {
+            if ($handler->match($tokens, $this->definitions)) {
+                if (Horde_Date_Parser::$debug) { echo "-date\n"; }
+                $goodTokens = array_filter($tokens, create_function('$o', 'return !$o->getTag("Separator");'));
+                return call_user_func(array($this, $handler->handlerMethod), $goodTokens, $options);
+            }
+        }
+
+        // I guess it's not a specific date, maybe it's just an anchor
+        foreach ($this->definitions['anchor'] as $handler) {
+            if ($handler->match($tokens, $this->definitions)) {
+                if (Horde_Date_Parser::$debug) { echo "-anchor\n"; }
+                $goodTokens = array_filter($tokens, create_function('$o', 'return !$o->getTag("Separator");'));
+                return call_user_func(array($this, $handler->handlerMethod), $goodTokens, $options);
+            }
+        }
+
+        // not an anchor, perhaps it's an arrow
+        foreach ($this->definitions['arrow'] as $handler) {
+            if ($handler->match($tokens, $this->definitions)) {
+                if (Horde_Date_Parser::$debug) { echo "-arrow\n"; }
+                $goodTokens = array_filter($tokens, create_function('$o', 'return !$o->getTag("SeparatorAt") && !$o->getTag("SeparatorSlashOrDash") && !$o->getTag("SeparatorComma");'));
+                return call_user_func(array($this, $handler->handlerMethod), $goodTokens, $options);
+            }
+        }
+
+        // not an arrow, let's hope it's a narrow
+        foreach ($this->definitions['narrow'] as $handler) {
+            if ($handler->match($tokens, $this->definitions)) {
+                if (Horde_Date_Parser::$debug) { echo "-narrow\n"; }
+                //good_tokens = tokens.select { |o| !o.get_tag Separator }
+                return call_user_func(array($this, $handler->handlerMethod), $tokens, $options);
+            }
+        }
+
+        // I guess you're out of luck!
+        if (Horde_Date_Parser::$debug) { echo "-none\n"; }
+        return null;
+    }
+
+
+    public function dayOrTime($dayStart, $timeTokens, $options)
+    {
+        $outerSpan = new Horde_Date_Span($dayStart, $dayStart + (24 * 60 * 60));
+
+        if (!empty($timeTokens)) {
+            $this->now = $outerSpan->begin;
+            return $this->getAnchor($this->dealiasAndDisambiguateTimes($timeTokens, $options), $options);
+        } else {
+            return $outerSpan;
+        }
+    }
+
+
+    public function handle_m_d($month, $day, $timeTokens, $options)
+    {
+        $month->start = $this->now;
+        $span = $month->this($options['context']);
+
+        $dayStart = new Horde_Date(array('year' => $span->begin->year, 'month' => $span->begin->month, 'day' => $day));
+        return $this->dayOrTime($dayStart, $timeTokens, $options);
+    }
+
+    public function handle_rmn_sd($tokens, $options)
+    {
+        return $this->handle_m_d($tokens[0]->getTag('RepeaterMonthName'), $tokens[1]->getTag('ScalarDay')->type, array_slice($tokens, 2), $options);
+    }
+
+    public function handle_rmn_od($tokens, $options)
+    {
+        return $this->handle_m_d($tokens[0]->getTag('RepeaterMonthName'), $tokens[1]->getTag('OrdinalDay')->type, array_slice($tokens, 2), $options);
+    }
+
+    public function handle_rmn_sy($tokens, $options)
+    {
+        $month = $tokens[0]->getTag('RepeaterMonthName')->index;
+        $year = $tokens[1]->getTag('ScalarYear')->type;
+
+        try {
+            return new Horde_Date_Span(new Horde_Date(array('year' => $year, 'month' => $month)), new Horde_Date(array('year' => $year, 'month' => $month + 1)));
+        } catch (Exception $e) {
+            return null;
+        }
+    }
+
+    public function handle_rdn_rmn_sd_t_tz_sy($tokens, $options)
+    {
+        $month = $tokens[1]->getTag('RepeaterMonthName')->index;
+        $day = $tokens[2]->getTag('ScalarDay')->type;
+        $year = $tokens[5]->getTag('ScalarYear')->type;
+
+        try {
+            $dayStart = new Horde_Date(array('year' => $year, 'month' => $month, 'day' => $day));
+            return $this->dayOrTime($daystart, array($tokens[3]), $options);
+        } catch (Exception $e) {
+            return null;
+        }
+    }
+
+    public function handle_rmn_sd_sy($tokens, $options)
+    {
+        $month = $tokens[0]->getTag('RepeaterMonthName')->index;
+        $day = $tokens[1]->getTag('ScalarDay')->type;
+        $year = $tokens[2]->getTag('ScalarYear')->type;
+
+        $timeTokens = array_slice($tokens, 3);
+
+        try {
+            $dayStart = new Horde_Date(array('year' => $year, 'month' => $month, 'day' => $day));
+            return $this->dayOrTime($dayStart, $timeTokens, $options);
+        } catch (Exception $e) {
+            return null;
+        }
+    }
+
+    public function handle_sd_rmn_sy($tokens, $options)
+    {
+        $newTokens = array($tokens[1], $tokens[0], $tokens[2]);
+        $timeTokens = array_slice($tokens, 3);
+        return $this->handle_rmn_sd_sy($newTokens + $timeTokens, $options);
+    }
+
+    public function handle_sm_sd_sy($tokens, $options)
+    {
+        $month = $tokens[0]->getTag('ScalarMonth')->type;
+        $day = $tokens[1]->getTag('ScalarDay')->type;
+        $year = $tokens[2]->getTag('ScalarYear')->type;
+
+        $timeTokens = array_slice($tokens, 3);
+
+        try {
+            $dayStart = new Horde_Date(array('year' => $year, 'month' => $month, 'day' => $day));
+            return $this->dayOrTime($dayStart, $timeTokens, $options);
+        } catch (Exception $e) {
+            return null;
+        }
+    }
+
+    public function handle_sd_sm_sy($tokens, $options)
+    {
+        $newTokens = array($tokens[1], $tokens[0], $tokens[2]);
+        $timeTokens = array_slice($tokens, 3);
+        return $this->handle_sm_sd_sy($newTokens + $timeTokens, $options);
+    }
+
+    public function handle_sy_sm_sd($tokens, $options)
+    {
+        $newTokens = array($tokens[1], $tokens[2], $tokens[0]);
+        $timeTokens = array_slice($tokens, 3);
+        return $this->handle_sm_sd_sy($newTokens + $timeTokens, $options);
+    }
+
+    public function handle_sm_sy($tokens, $options)
+    {
+        $month = $tokens[0]->getTag('ScalarMonth')->type;
+        $year = $tokens[1]->getTag('ScalarYear')->type;
+
+        try {
+            return new Horde_Date_Span(new Horde_Date(array('year' => $year, 'month' => $month)), new Horde_Date(array('year' => $year, 'month' => $month = 1)));
+        } catch (Exception $e) {
+            return null;
+        }
+    }
+
+
+    /*##########################################################################
+    # Anchors
+    ##########################################################################*/
+
+    public function handle_r($tokens, $options)
+    {
+        $ddTokens = $this->dealiasAndDisambiguateTimes($tokens, $options);
+        return $this->getAnchor($ddTokens, $options);
+    }
+
+    public function handle_r_g_r($tokens, $options)
+    {
+        $newTokens = array($tokens[1], $tokens[0], $tokens[2]);
+        return $this->handle_r($newTokens, $options);
+    }
+
+
+    /*##########################################################################
+    # Arrows
+    ##########################################################################*/
+
+    public function handle_srp($tokens, $span, $options)
+    {
+        $distance = $tokens[0]->getTag('Scalar')->type;
+        $repeater = $tokens[1]->getTag('Repeater');
+        $pointer = $tokens[2]->getTag('Pointer')->type;
+
+        return $repeater->offset($span, $distance, $pointer);
+    }
+
+    public function handle_s_r_p($tokens, $options)
+    {
+        $repeater = $tokens[1]->getTag('Repeater');
+
+        /*
+          # span =
+          # case true
+          # when [RepeaterYear, RepeaterSeason, RepeaterSeasonName, RepeaterMonth, RepeaterMonthName, RepeaterFortnight, RepeaterWeek].include?(repeater.class)
+          #   self.parse("this hour", :guess => false, :now => @now)
+          # when [RepeaterWeekend, RepeaterDay, RepeaterDayName, RepeaterDayPortion, RepeaterHour].include?(repeater.class)
+          #   self.parse("this minute", :guess => false, :now => @now)
+          # when [RepeaterMinute, RepeaterSecond].include?(repeater.class)
+          #   self.parse("this second", :guess => false, :now => @now)
+          # else
+          #   raise(ChronicPain, "Invalid repeater: #{repeater.class}")
+          # end
+        */
+
+        $span = $this->parse('this second', array('guess' => false, 'now' => $this->now));
+        return $this->handle_srp($tokens, $span, $options);
+    }
+
+    public function handle_p_s_r($tokens, $options)
+    {
+        $newTokens = array($tokens[1], $tokens[2], $tokens[0]);
+        return $this->handle_s_r_p($newTokens, $options);
+    }
+
+    public function handle_s_r_p_a($tokens, $options)
+    {
+        $anchorSpan = $this->getAnchor(array_slice($tokens, 3), $options);
+        return $this->handle_srp($tokens, $anchorSpan, $options);
+    }
+
+
+    /*##########################################################################
+    # Narrows
+    ##########################################################################*/
+
+    public function handle_orr($tokens, $outerSpan, $options)
+    {
+        $repeater = $tokens[1]->getTag('Repeater');
+        $repeater->start = $outerSpan->begin - 1;
+        $ordinal = $tokens[0]->getTag('Ordinal')->type;
+        $span = null;
+
+        for ($i = 0; $i < $ordinal; $i++) {
+            $span = $repeater->next('future');
+            if ($span->begin > $outerSpan->end) {
+                $span = null;
+                break;
+            }
+        }
+        return $span;
+    }
+
+    public function handle_o_r_s_r($tokens, $options)
+    {
+        $outerSpan = $this->getAnchor(array($tokens[3]), $options);
+        return $this->handle_orr(array($tokens[0], $tokens[1]), $outerSpan, $options);
+    }
+
+    public function handle_o_r_g_r($tokens, $options)
+    {
+        $outerSpan = $this->getAnchor(array($tokens[2], $tokens[3]), $options);
+        return $this->handle_orr(array($tokens[0], $tokens[1]), $outerSpan, $options);
+    }
+
+
+    /*##########################################################################
+    # Support Methods
+    ##########################################################################*/
+
+    public function getAnchor($tokens, $options)
+    {
+        $grabber = $this->componentFactory('Grabber', array('this'));
+        $pointer = 'future';
+
+        $repeaters = $this->getRepeaters($tokens);
+        for ($i = 0, $size = count($repeaters); $i < $size; $i++) {
+            array_pop($tokens);
+        }
+
+        if (count($tokens) && $tokens[0]->getTag('Grabber')) {
+            $grabber = $tokens[0]->getTag('Grabber');
+            array_pop($tokens);
+        }
+
+        $head = array_shift($repeaters);
+        $head->start = $this->now;
+
+        switch ($grabber->type) {
+        case 'last':
+            $outerSpan = $head->next('past');
+            break;
+
+        case 'this':
+            if (count($repeaters)) {
+                $outerSpan = $head->this('none');
+            } else {
+                $outerSpan = $head->this($options['context']);
+            }
+            break;
+
+        case 'next':
+            $outerSpan = $head->next('future');
+            break;
+
+        default:
+            throw new Horde_Date_Parser_Exception('Invalid grabber ' . $grabber->type);
+        }
+
+        if (Horde_Date_Parser::$debug) { echo "--$outerSpan\n"; }
+        return $this->findWithin($repeaters, $outerSpan, $pointer);
+    }
+
+    public function getRepeaters($tokens)
+    {
+        $repeaters = array();
+        foreach ($tokens as $token) {
+            if ($t = $token->getTag('Repeater')) {
+                $repeaters[] = $t;
+            }
+        }
+
+        rsort($repeaters);
+        return $repeaters;
+    }
+
+    /**
+     * Recursively finds repeaters within other repeaters.  Returns a Span
+     * representing the innermost time span or null if no repeater union could
+     * be found
+     */
+    public function findWithin($tags, $span, $pointer)
+    {
+        if (Horde_Date_Parser::$debug) { echo "--$span\n"; }
+        if (empty($tags)) { return $span; }
+
+        $head = array_shift($tags);
+        $rest = $tags;
+        $head->start = ($pointer == 'future') ? $span->begin : $span->end;
+        $h = $head->this('none');
+
+        if ($span->include($h->begin) || $span->include($h->end)) {
+            return $this->findWithin($rest, $h, $pointer);
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * handle aliases of am/pm
+     * 5:00 in the morning -> 5:00 am
+     * 7:00 in the evening -> 7:00 pm
+     */
+    public function dealiasAndDisambiguateTimes($tokens, $options)
+    {
+        $dayPortionIndex = null;
+        foreach ($tokens as $i => $t) {
+            if ($t->getTag('RepeaterDayPortion')) {
+                $dayPortionIndex = $i;
+                break;
+            }
+        }
+
+        $timeIndex = null;
+        foreach ($tokens as $i => $t) {
+            if ($t->getTag('RepeaterTime')) {
+                $timeIndex = $i;
+                break;
+            }
+        }
+
+        if ($dayPortionIndex && $timeIndex) {
+            $t1 = $tokens[$dayPortionIndex];
+            $t1tag = $t1->getTag('RepeaterDayPortion');
+
+            if ($t1tag->type == 'morning') {
+                if (Horde_Date_Parser::$debug) { echo "--morning->am\n"; }
+                $t1->untag('RepeaterDayPortion');
+                $t1->tag(new Horde_Date_Parser_Locale_Base_RepeaterDayPortion('am'));
+            } elseif (in_array($t1tag->type, array('afternoon', 'evening', 'night'))) {
+                if (Horde_Date_Parser::$debug) { echo "--{$t1tag->type}->pm\n"; }
+                $t1->untag('RepeaterDayPortion');
+                $t1->tag(new Horde_Date_Parser_Locale_Base_RepeaterDayPortion('pm'));
+            }
+        }
+
+        /*
+          # tokens.each_with_index do |t0, i|
+          #   t1 = tokens[i + 1]
+          #   if t1 && (t1tag = t1.get_tag(RepeaterDayPortion)) && t0.get_tag(RepeaterTime)
+          #     if [:morning].include?(t1tag.type)
+          #       puts '--morning->am' if Chronic.debug
+          #       t1.untag(RepeaterDayPortion)
+          #       t1.tag(RepeaterDayPortion.new(:am))
+          #     elsif [:afternoon, :evening, :night].include?(t1tag.type)
+          #       puts "--#{t1tag.type}->pm" if Chronic.debug
+          #       t1.untag(RepeaterDayPortion)
+          #       t1.tag(RepeaterDayPortion.new(:pm))
+          #     end
+          #   end
+          # end
+        */
+
+        // handle ambiguous times if :ambiguousTimeRange is specified
+        if ($options['ambiguousTimeRange'] != 'none') {
+            $ttokens = array();
+            foreach ($tokens as $i => $t0) {
+                $ttokens[] = $t0;
+                $t1 = isset($tokens[$i + 1]) ? $tokens[$i + 1] : null;
+                if ($t0->getTag('RepeaterTime') && $t0->getTag('RepeaterTime')->type->ambiguous() && (!$t1 || !$t1->getTag('RepeaterDayPortion'))) {
+                    $distoken = new Horde_Date_Parser_Token('disambiguator');
+                    $distoken->tag(new Horde_Date_Parser_Locale_Base_RepeaterDayPortion($options['ambiguousTimeRange']));
+                    $ttokens[] = $distoken;
+                }
+            }
+
+            $tokens = $ttokens;
+        }
+
+        return $tokens;
+    }
 
 }