iainbeeston/nickel

View on GitHub
lib/nickel/construct_finder.rb

Summary

Maintainability
F
2 wks
Test Coverage
require 'nickel/construct'
require 'nickel/zdate'
require 'nickel/ztime'

module Nickel
  class ConstructFinder
    attr_reader :constructs, :components

    def initialize(query, curdate, curtime)
      @curdate = curdate
      @curtime = curtime
      @components = query.split
      @pos = 0    # iterator
      @constructs = []
    end

    def run
      while @pos < @components.size
        big_if_on_current_word
        @pos += 1
      end
    end

    def reset_instance_vars
      @day_index = nil
      @month_index = nil
      @week_num = nil
      @date_array = nil
      @length = nil
      @time1 = nil
      @time2 = nil
      @date1 = nil
      @date2 = nil
    end

    def big_if_on_current_word
      reset_instance_vars

      if match_every
        if match_every_dayname
          found_every_dayname                             # every tue
        elsif match_every_day
          found_every_day                                 # every day
        elsif match_every_other
          if match_every_other_dayname
            found_every_other_dayname                     # every other fri
          elsif match_every_other_day
            found_every_other_day                         # every other day
          end
        elsif match_every_3rd
          if match_every_3rd_dayname
            found_every_3rd_dayname                       # every third fri
          elsif match_every_3rd_day
            found_every_3rd_day                           # every third day
          end
        end

      elsif match_repeats
        if match_repeats_daily
          found_repeats_daily                             # repeats daily
        elsif match_repeats_altdaily
          found_repeats_altdaily                          # repeats altdaily
        elsif match_repeats_weekly_vague
          found_repeats_weekly_vague                      # repeats weekly
        elsif match_repeats_altweekly_vague
          found_repeats_altweekly_vague                   # repeats altweekly
        elsif match_repeats_monthly
          if match_repeats_daymonthly
            found_repeats_daymonthly                      # repeats monthly 1st fri
          elsif match_repeats_datemonthly
            found_repeats_datemonthly                     # repeats monthly 22nd
          end
        elsif match_repeats_altmonthly
          if match_repeats_altmonthly_daymonthly
            found_repeats_altmonthly_daymonthly           # repeats altmonthly 1st fri
          elsif match_repeats_altmonthly_datemonthly
            found_repeats_altmonthly_datemonthly          # repeats altmonthly 22nd
          end
        elsif match_repeats_threemonthly
          if match_repeats_threemonthly_daymonthly
            found_repeats_threemonthly_daymonthly         # repeats threemonthly 1st fri
          elsif match_repeats_threemonthly_datemonthly
            found_repeats_threemonthly_datemonthly        # repeats threemonthly 22nd
          end
        end

      elsif match_for_x
        if match_for_x_days
          found_for_x_days                                # for 10 days
        elsif match_for_x_weeks
          found_for_x_weeks                               # for 10 weeks
        elsif match_for_x_months
          found_for_x_months                              # for 10 months
        end

      elsif match_this
        if match_this_dayname
          found_this_dayname                              # this fri
        elsif match_this_week
          found_this_week                                 # this week
        elsif match_this_month
          found_this_month                                # this month (implies 9/1 to 9/30)
        end                                                                                                 # SHOULDN'T "this" HAVE "this weekend" ???

      elsif match_next
        if match_next_weekend
          found_next_weekend                              # next weekend --- never hit?
        elsif match_next_dayname
          found_next_dayname                              # next tuesday
        elsif match_next_x
          if match_next_x_days
            found_next_x_days                             # next 5 days   --- shouldn't this be a wrapper?
          elsif match_next_x_weeks
            found_next_x_weeks                            # next 5 weeks  --- shouldn't this be a wrapper?
          elsif match_next_x_months
            found_next_x_months                           # next 5 months --- shouldn't this be a wrapper?
          elsif match_next_x_years
            found_next_x_years                            # next 5 years  --- shouldn't this be a wrapper?
          end
        elsif match_next_week
          found_next_week
        elsif match_next_month
          found_next_month                                # next month (implies 10/1 to 10/31)
        end

      elsif match_week
        if match_week_of_date
          found_week_of_date                              # week of 1/2
        elsif match_week_through_date
          found_week_through_date                         # week through 1/2  (as in, week ending 1/2)
        end

      elsif match_x_weeks_from
        if match_x_weeks_from_dayname
          found_x_weeks_from_dayname                      # 5 weeks from tuesday
        elsif match_x_weeks_from_this_dayname
          found_x_weeks_from_this_dayname                 # 5 weeks from this tuesday
        elsif match_x_weeks_from_next_dayname
          found_x_weeks_from_next_dayname                 # 5 weeks from next tuesday
        elsif match_x_weeks_from_tomorrow
          found_x_weeks_from_tomorrow                     # 5 weeks from tomorrow
        elsif match_x_weeks_from_now
          found_x_weeks_from_now                          # 5 weeks from now
        elsif match_x_weeks_from_yesterday
          found_x_weeks_from_yesterday                    # 5 weeks from yesterday
        end

      elsif match_x_months_from
        if match_x_months_from_dayname
          found_x_months_from_dayname                   # 2 months from wed
        elsif match_x_months_from_this_dayname
          found_x_months_from_this_dayname              # 2 months from this wed
        elsif match_x_months_from_next_dayname
          found_x_months_from_next_dayname              # 2 months from next wed
        elsif match_x_months_from_tomorrow
          found_x_months_from_tomorrow                  # 2 months from tomorrow
        elsif match_x_months_from_now
          found_x_months_from_now                       # 2 months from now
        elsif match_x_months_from_yesterday
          found_x_months_from_yesterday                 # 2 months from yesterday
        end

      elsif match_x_days_from
        if match_x_days_from_now
          found_x_days_from_now                         # 5 days from now
        elsif match_x_days_from_dayname
          found_x_days_from_dayname                     # 5 days from monday
        end

      elsif match_x_dayname_from
        if match_x_dayname_from_now
          found_x_dayname_from_now                      # 2 fridays from now
        elsif match_x_dayname_from_tomorrow
          found_x_dayname_from_tomorrow                 # 2 fridays from tomorrow
        elsif match_x_dayname_from_yesterday
          found_x_dayname_from_yesterday                # 2 fridays from yesterday
        elsif match_x_dayname_from_this
          found_x_dayname_from_this                     # 2 fridays from this one
        elsif match_x_dayname_from_next
          found_x_dayname_from_next                     # 2 fridays from next friday
        end

      elsif match_x_minutes_from_now
        found_x_minutes_from_now                        # 5 minutes from now
      elsif match_x_hours_from_now
        found_x_hours_from_now                          # 5 hours from now

      elsif match_ordinal_dayname
        if match_ordinal_dayname_this_month
          found_ordinal_dayname_this_month              # 2nd friday this month
        elsif match_ordinal_dayname_next_month
          found_ordinal_dayname_next_month              # 2nd friday next month
        elsif match_ordinal_dayname_monthname
          found_ordinal_dayname_monthname               # 2nd friday december
        end

      elsif match_ordinal_this_month
        found_ordinal_this_month                        # 28th this month
      elsif match_ordinal_next_month
        found_ordinal_next_month                        # 28th next month

      elsif match_first_day
        if match_first_day_this_month
          found_first_day_this_month                    # first day this month
        elsif match_first_day_next_month
          found_first_day_next_month                    # first day next month
        elsif match_first_day_monthname
          found_first_day_monthname                     # first day january (well this is stupid, "first day of january" gets preprocessed into "1/1", so what is the point of this?)
        end

      elsif match_last_day
        if match_last_day_this_month
          found_last_day_this_month                     # last day this month
        elsif match_last_day_next_month
          found_last_day_next_month                     # last day next month
        elsif match_last_day_monthname
          found_last_day_monthname                      # last day november
        end

      elsif match_at
        if match_at_time
          if match_at_time_through_time
            found_at_time_through_time                  # at 2 through 5pm
          else
            found_at_time                               # at 2
          end
        end

      elsif match_all_day
        found_all_day                                   # all day

      elsif match_tomorrow
        if match_tomorrow_through
          if match_tomorrow_through_dayname
            found_tomorrow_through_dayname              # tomorrow through friday
          elsif match_tomorrow_through_date
            found_tomorrow_through_date                 # tomorrow through august 20th
          end
        else
          found_tomorrow                                # tomorrow
        end

      elsif match_now
        if match_now_through
          if match_now_through_dayname
            found_now_through_dayname                   # today through friday
          elsif match_now_through_following_dayname
            found_now_through_following_dayname         # REDUNDANT, PREPROCESS THIS OUT
          elsif match_now_through_date
            found_now_through_date                      # today through 10/1
          elsif match_now_through_tomorrow
            found_now_through_tomorrow                  # today through tomorrow
          elsif match_now_through_next_dayname
            found_now_through_next_dayname              # today through next friday
          end
        else
          found_now                                     # today
        end

      elsif match_dayname
        if match_dayname_the_ordinal
          found_dayname_the_ordinal                     # monday the 21st
        elsif match_dayname_x_weeks_from_next
          found_dayname_x_weeks_from_next               # monday 2 weeks from next
        elsif match_dayname_x_weeks_from_this
          found_dayname_x_weeks_from_this               # monday 2 weeks from this
        else
          found_dayname                                 # monday (also monday tuesday wed...)
        end

      elsif match_through_monthname
        found_through_monthname                         # through december (implies through 11/30)
      elsif match_monthname
        found_monthname                                 # december (implies 12/1 to 12/31)

      # 5th constructor
      elsif match_start
        found_start
      elsif match_through
        found_through

      elsif match_time                                  # match time second to last
        if match_time_through_time
          found_time_through_time                       # 10 to 4
        else
          found_time                                    # 10
        end

      elsif match_date                                  # match date last
        if match_date_through_date
          found_date_through_date                       # 5th through the 16th
        else
          found_date                                    # 5th
        end
      end
    end # end def big_if_on_current_word

    def match_every
      @components[@pos] == 'every'
    end

    def match_every_dayname
      @day_index = ZDate.days_of_week.index(@components[@pos + 1])     # if "every [day]"
    end

    def found_every_dayname
      day_array = [@day_index]
      j = 2
      while @components[@pos + j] && ZDate.days_of_week.index(@components[@pos + j]) # if "every mon tue wed"
        day_array << ZDate.days_of_week.index(@components[@pos + j])
        j += 1
      end
      @constructs << RecurrenceConstruct.new(repeats: :weekly, repeats_on: day_array, comp_start: @pos, comp_end: @pos += (j - 1), found_in: __method__)
    end

    def match_every_day
      @components[@pos + 1] == 'day'
    end

    def found_every_day
      @constructs << RecurrenceConstruct.new(repeats: :daily, comp_start: @pos, comp_end: @pos += 1, found_in: __method__)
    end

    def match_every_other
      @components[@pos + 1] =~ /other|2nd/
    end

    def match_every_other_dayname
      @day_index = ZDate.days_of_week.index(@components[@pos + 2])      # if "every other mon"
    end

    def found_every_other_dayname
      day_array = [@day_index]
      j = 3
      while @components[@pos + j] && ZDate.days_of_week.index(@components[@pos + j])  # if "every other mon tue wed
        day_array << ZDate.days_of_week.index(@components[@pos + j])
        j += 1
      end
      @constructs << RecurrenceConstruct.new(repeats: :altweekly, repeats_on: day_array, comp_start: @pos, comp_end: @pos += (j - 1), found_in: __method__)
    end

    def match_every_other_day
      @components[@pos + 2] == 'day'       #  if "every other day"
    end

    def found_every_other_day
      @constructs << RecurrenceConstruct.new(repeats: :altdaily, comp_start: @pos, comp_end: @pos += 2, found_in: __method__)
    end

    def match_every_3rd
      @components[@pos + 1] == '3rd'
    end

    def match_every_3rd_dayname
      @day_index = ZDate.days_of_week.index(@components[@pos + 2])      # if "every 3rd tue"
    end

    def found_every_3rd_dayname
      day_array = [@day_index]
      j = 3
      while @components[@pos + j] && ZDate.days_of_week.index(@components[@pos + j])  # if "every 3rd tue wed thu
        day_array << ZDate.days_of_week.index(@components[@pos + j])
        j += 1
      end
      @constructs << RecurrenceConstruct.new(repeats: :threeweekly, repeats_on: day_array, comp_start: @pos, comp_end: @pos += (j - 1), found_in: __method__)
    end

    def match_every_3rd_day
      @components[@pos + 2] == 'day'       #  if "every 3rd day"
    end

    def found_every_3rd_day
      @constructs << RecurrenceConstruct.new(repeats: :threedaily, comp_start: @pos, comp_end: @pos += 2, found_in: __method__)
    end

    def match_repeats
      @components[@pos] == 'repeats'
    end

    def match_repeats_daily
      @components[@pos + 1] == 'daily'
    end

    def found_repeats_daily
      @constructs << RecurrenceConstruct.new(repeats: :daily, comp_start: @pos, comp_end: @pos += 1, found_in: __method__)
    end

    def match_repeats_altdaily
      @components[@pos + 1] == 'altdaily'
    end

    def found_repeats_altdaily
      @constructs << RecurrenceConstruct.new(repeats: :altdaily, comp_start: @pos, comp_end: @pos += 1, found_in: __method__)
    end

    def match_repeats_weekly_vague
      @components[@pos + 1] == 'weekly'
    end

    def found_repeats_weekly_vague
      @constructs << RecurrenceConstruct.new(repeats: :weekly, comp_start: @pos, comp_end: @pos += 1, found_in: __method__)
    end

    def match_repeats_altweekly_vague
      @components[@pos + 1] == 'altweekly'
    end

    def found_repeats_altweekly_vague
      @constructs << RecurrenceConstruct.new(repeats: :altweekly, comp_start: @pos, comp_end: @pos += 1, found_in: __method__)
    end

    def match_repeats_monthly
      @components[@pos + 1] == 'monthly'
    end

    def match_repeats_daymonthly
      @components[@pos + 2] && @components[@pos + 3] && (@week_num = @components[@pos + 2].to_i) && @week_num > 0 && @week_num <= 5 && (@day_index = ZDate.days_of_week.index(@components[@pos + 3]))   # "repeats monthly 2nd wed"
    end

    def found_repeats_daymonthly
      rep_array = [[@week_num, @day_index]]     # That is NOT a typo, not sure what I meant by that! maybe the nested array
      j = 4
      while @components[@pos + j] && @components[@pos + j + 1] && (@week_num = @components[@pos + j].to_i) && @week_num > 0 && @week_num <= 5 && (@day_index = ZDate.days_of_week.index(@components[@pos + j + 1]))
        rep_array << [@week_num, @day_index]
        j += 2
      end
      @constructs << RecurrenceConstruct.new(repeats: :daymonthly, repeats_on: rep_array, comp_start: @pos, comp_end: @pos += (j - 1), found_in: __method__)
    end

    def match_repeats_datemonthly
      @components[@pos + 2] && ConstructFinder.ordinal_only?(@components[@pos + 2]) && @date_array = [@components[@pos + 2].to_i]   # repeats monthly 22nd
    end

    def found_repeats_datemonthly
      j = 3
      while @components[@pos + j] && ConstructFinder.ordinal_only?(@components[@pos + j])
        @date_array << @components[@pos + j].to_i
        j += 1
      end
      @constructs << RecurrenceConstruct.new(repeats: :datemonthly, repeats_on: @date_array, comp_start: @pos, comp_end: @pos += (j - 1), found_in: __method__)
    end

    def match_repeats_altmonthly
      @components[@pos + 1] == 'altmonthly'
    end

    def match_repeats_altmonthly_daymonthly
      @components[@pos + 2] && @components[@pos + 3] && (@week_num = @components[@pos + 2].to_i) && @week_num > 0 && @week_num <= 5 && (@day_index = ZDate.days_of_week.index(@components[@pos + 3]))   # "repeats altmonthly 2nd wed"
    end

    def found_repeats_altmonthly_daymonthly
      rep_array = [[@week_num, @day_index]]
      j = 4
      while @components[@pos + j] && @components[@pos + j + 1] && (@week_num = @components[@pos + j].to_i) && @week_num > 0 && @week_num <= 5 && (@day_index = ZDate.days_of_week.index(@components[@pos + j + 1]))
        rep_array << [@week_num, @day_index]
        j += 2
      end
      @constructs << RecurrenceConstruct.new(repeats: :altdaymonthly, repeats_on: rep_array, comp_start: @pos, comp_end: @pos += (j - 1), found_in: __method__)
    end

    def match_repeats_altmonthly_datemonthly
      @components[@pos + 2] && ConstructFinder.ordinal_only?(@components[@pos + 2]) && @date_array = [@components[@pos + 2].to_i]   # repeats altmonthly 22nd
    end

    def found_repeats_altmonthly_datemonthly
      j = 3
      while @components[@pos + j] && ConstructFinder.ordinal_only?(@components[@pos + j])
        @date_array << @components[@pos + j].to_i
        j += 1
      end
      @constructs << RecurrenceConstruct.new(repeats: :altdatemonthly, repeats_on: @date_array, comp_start: @pos, comp_end: @pos += (j - 1), found_in: __method__)
    end

    def match_repeats_threemonthly
      @components[@pos + 1] == 'threemonthly'
    end

    def match_repeats_threemonthly_daymonthly
      @components[@pos + 2] && @components[@pos + 3] && (@week_num = @components[@pos + 2].to_i) && @week_num > 0 && @week_num <= 5 && (@day_index = ZDate.days_of_week.index(@components[@pos + 3]))   # "repeats threemonthly 2nd wed"
    end

    def found_repeats_threemonthly_daymonthly
      rep_array = [[@week_num, @day_index]]     # That is NOT a typo
      j = 4
      while @components[@pos + j] && @components[@pos + j + 1] && (@week_num = @components[@pos + j].to_i) && @week_num > 0 && @week_num <= 5 && (@day_index = ZDate.days_of_week.index(@components[@pos + j + 1]))
        rep_array << [@week_num, @day_index]
        j += 2
      end
      @constructs << RecurrenceConstruct.new(repeats: :threedaymonthly, repeats_on: rep_array, comp_start: @pos, comp_end: @pos += (j - 1), found_in: __method__)
    end

    def match_repeats_threemonthly_datemonthly
      @components[@pos + 2] && ConstructFinder.ordinal_only?(@components[@pos + 2]) && @date_array = [@components[@pos + 2].to_i]   # repeats threemonthly 22nd
    end

    def found_repeats_threemonthly_datemonthly
      j = 3
      while @components[@pos + j] && ConstructFinder.ordinal_only?(@components[@pos + j])
        @date_array << @components[@pos + j].to_i
        j += 1
      end
      @constructs << RecurrenceConstruct.new(repeats: :threedatemonthly, repeats_on: @date_array, comp_start: @pos, comp_end: @pos += (j - 1), found_in: __method__)
    end

    def match_for_x
      @components[@pos] == 'for' && ConstructFinder.digits_only?(@components[@pos + 1]) && @length = @components[@pos + 1].to_i
    end

    def match_for_x_days
      @components[@pos + 2] =~ /days?/
    end

    def found_for_x_days
      @constructs << WrapperConstruct.new(wrapper_type: 2, wrapper_length: @length, comp_start: @pos, comp_end: @pos += 2, found_in: __method__)
    end

    def match_for_x_weeks
      @components[@pos + 2] =~ /weeks?/
    end

    def found_for_x_weeks
      @constructs << WrapperConstruct.new(wrapper_type: 3, wrapper_length: @length, comp_start: @pos, comp_end: @pos += 2, found_in: __method__)
    end

    def match_for_x_months
      @components[@pos + 2] =~ /months?/
    end

    def found_for_x_months
      @constructs << WrapperConstruct.new(wrapper_type: 4, wrapper_length: @length, comp_start: @pos, comp_end: @pos += 2, found_in: __method__)
    end

    def match_this
      @components[@pos] == 'this'
    end

    def match_this_dayname
      @day_index = ZDate.days_of_week.index(@components[@pos + 1])
    end

    def found_this_dayname
      day_to_add = @curdate.this(@day_index)
      @constructs << DateConstruct.new(date: day_to_add, comp_start: @pos, comp_end: @pos += 1, found_in: __method__)
      while @components[@pos + 1] && @day_index = ZDate.days_of_week.index(@components[@pos + 1])
        # note @pos gets incremented on each pass
        @constructs << DateConstruct.new(date: day_to_add = day_to_add.this(@day_index), comp_start: @pos + 1, comp_end: @pos += 1, found_in: __method__)
      end
    end

    def match_this_week
      @components[@pos + 1] =~ /weeks?/
    end

    def found_this_week
      @constructs << DateSpanConstruct.new(start_date: @curdate, end_date: @curdate.add_days(7), comp_start: @pos, comp_end: @pos += 1, found_in: __method__)
    end

    def match_this_month
      @components[@pos + 1] =~ /months?/
    end

    def found_this_month
      @constructs << DateSpanConstruct.new(start_date: @curdate, end_date: @curdate.end_of_month, comp_start: @pos, comp_end: @pos += 1, found_in: __method__)
    end

    def match_next
      @components[@pos] == 'next'
    end

    def match_next_weekend
      @components[@pos + 1] == 'weekend'   # "next weekend"
    end

    def found_next_weekend
      dsc = DateSpanConstruct.new(start_date: @curdate.next(5), comp_start: @pos, comp_end: @pos += 1, found_in: __method__)
      dsc.end_date = dsc.start_date.add_days(1)
      @constructs << dsc
    end

    def match_next_dayname
      @day_index = ZDate.days_of_week.index(@components[@pos + 1])  # if "next [day]"
    end

    def found_next_dayname
      day_to_add = @curdate.next(@day_index)
      @constructs << DateConstruct.new(date: day_to_add, comp_start: @pos, comp_end: @pos += 1, found_in: __method__)
      while @components[@pos + 1] && @day_index = ZDate.days_of_week.index(@components[@pos + 1])
        # note @pos gets incremented on each pass
        @constructs << DateConstruct.new(date: day_to_add = day_to_add.this(@day_index), comp_start: @pos + 1, comp_end: @pos += 1, found_in: __method__)
      end
    end

    def match_next_x
      @components[@pos + 1] && ConstructFinder.digits_only?(@components[@pos + 1]) && @length = @components[@pos + 1].to_i
    end

    def match_next_x_days
      @components[@pos + 2] =~ /days?/                              # "next x days"
    end

    def found_next_x_days
      @constructs << DateSpanConstruct.new(start_date: @curdate, end_date: @curdate.add_days(@length), comp_start: @pos, comp_end: @pos += 2, found_in: __method__)
    end

    def match_next_x_weeks
      @components[@pos + 2] =~ /weeks?/                             # "next x weeks"
    end

    def found_next_x_weeks
      @constructs << DateSpanConstruct.new(start_date: @curdate, end_date: @curdate.add_weeks(@length), comp_start: @pos, comp_end: @pos += 2, found_in: __method__)
    end

    def match_next_x_months
      @components[@pos + 2] =~ /months?/                             # "next x months"
    end

    def found_next_x_months
      @constructs << DateSpanConstruct.new(start_date: @curdate, end_date: @curdate.add_months(@length), comp_start: @pos, comp_end: @pos += 2, found_in: __method__)
    end

    def match_next_x_years
      @components[@pos + 2] =~ /years?/                          # "next x years"
    end

    def found_next_x_years
      @constructs << DateSpanConstruct.new(start_date: @curdate, end_date: @curdate.add_years(@length), comp_start: @pos, comp_end: @pos += 2, found_in: __method__)
    end

    def match_next_week
      @components[@pos + 1] =~ /weeks?/
    end

    def found_next_week
      sd = @curdate.add_days(7)
      ed = sd.add_days(7)
      @constructs << DateSpanConstruct.new(start_date: sd, end_date: ed, comp_start: @pos, comp_end: @pos += 1, found_in: __method__)
    end

    def match_next_month
      # note it is important that all other uses of "next month" come after indicating words such as "every day next month"; otherwise they will be converted here
      @components[@pos + 1] =~ /months?/
    end

    def found_next_month
      sd = @curdate.add_months(1).beginning_of_month
      ed = sd.end_of_month
      @constructs << DateSpanConstruct.new(start_date: sd, end_date: ed, comp_start: @pos, comp_end: @pos += 1, found_in: __method__)
    end

    def match_week
      @components[@pos] == 'week'
    end

    def match_week_of_date
      @components[@pos + 1] == 'of' && @date1 = ZDate.interpret(@components[@pos + 2], @curdate)
    end

    def found_week_of_date
      @constructs << DateSpanConstruct.new(start_date: @date1, end_date: @date1.add_days(7), comp_start: @pos, comp_end: @pos += 2, found_in: __method__)
    end

    def match_week_through_date
      @components[@pos + 1] == 'through' && @date1 = ZDate.interpret(@components[@pos + 2], @curdate)
    end

    def found_week_through_date
      @constructs << DateSpanConstruct.new(start_date: @date1.sub_days(7), end_date: @date1, comp_start: @pos, comp_end: @pos += 2, found_in: __method__)
    end

    def match_x_weeks_from
      ConstructFinder.digits_only?(@components[@pos]) && @components[@pos + 1] =~ /^weeks?$/ && @components[@pos + 2] == 'from' && @length = @components[@pos].to_i      # if "x weeks from"
    end

    def match_x_weeks_from_dayname
      @day_index = ZDate.days_of_week.index(@components[@pos + 3])   # if "x weeks from monday"
    end

    def found_x_weeks_from_dayname
      @constructs << DateConstruct.new(date: @curdate.x_weeks_from_day(@length, @day_index), comp_start: @pos, comp_end: @pos += 3, found_in: __method__)
    end

    # Reduntant, preprocess out!
    def match_x_weeks_from_this_dayname
      @components[@pos + 3] == 'this' && @day_index = ZDate.days_of_week.index(@components[@pos + 4])           # if "x weeks from this monday"
    end

    # Reduntant, preprocess out!
    def found_x_weeks_from_this_dayname
      # this is the exact some construct as found_x_weeks_from_dayname, just position and comp_end has to increment by 1 more; pretty stupid, this should be caught in preprocessing
      @constructs << DateConstruct.new(date: @curdate.x_weeks_from_day(@length, @day_index), comp_start: @pos, comp_end: @pos += 4, found_in: __method__)
    end

    def match_x_weeks_from_next_dayname
      @components[@pos + 3] == 'next' && @day_index = ZDate.days_of_week.index(@components[@pos + 4])   # if "x weeks from next monday"
    end

    def found_x_weeks_from_next_dayname
      @constructs << DateConstruct.new(date: @curdate.x_weeks_from_day(@length + 1, @day_index), comp_start: @pos, comp_end: @pos += 4, found_in: __method__)
    end

    def match_x_weeks_from_tomorrow
      @components[@pos + 3] == 'tomorrow'       # if "x weeks from tomorrow"
    end

    def found_x_weeks_from_tomorrow
      @constructs << DateConstruct.new(date: @curdate.add_days(1).add_weeks(@length), comp_start: @pos, comp_end: @pos += 3, found_in: __method__)
    end

    def match_x_weeks_from_now
      @components[@pos + 3] =~ /\b(today)|(now)\b/    # if "x weeks from today"
    end

    def found_x_weeks_from_now
      @constructs << DateConstruct.new(date: @curdate.x_weeks_from_day(@length, @curdate.dayindex), comp_start: @pos, comp_end: @pos += 3, found_in: __method__)
    end

    def match_x_weeks_from_yesterday
      @components[@pos + 3] == 'yesterday'    # "x weeks from yesterday"
    end

    def found_x_weeks_from_yesterday
      @constructs << DateConstruct.new(date: @curdate.sub_days(1).add_weeks(@length), comp_start: @pos, comp_end: @pos += 3, found_in: __method__)
    end

    def match_x_months_from
      ConstructFinder.digits_only?(@components[@pos]) && @components[@pos + 1] =~ /^months?$/ && @components[@pos + 2] == 'from' && @length = @components[@pos].to_i       # if "x months from"
    end

    def match_x_months_from_dayname
      @day_index = ZDate.days_of_week.index(@components[@pos + 3])                                             # if "x months from monday"
    end

    def found_x_months_from_dayname
      @constructs << DateConstruct.new(date: @curdate.this(@day_index).add_months(@length), comp_start: @pos, comp_end: @pos += 3, found_in: __method__)
    end

    def match_x_months_from_this_dayname
      @components[@pos + 3] == 'this' && @day_index = ZDate.days_of_week.index(@components[@pos + 4])            # if "x months from this monday"
    end

    def found_x_months_from_this_dayname
      @constructs << DateConstruct.new(date: @curdate.this(@day_index).add_months(@length), comp_start: @pos, comp_end: @pos += 4, found_in: __method__)
    end

    def match_x_months_from_next_dayname
      @components[@pos + 3] == 'next' && @day_index = ZDate.days_of_week.index(@components[@pos + 4])            # if "x months from next monday"
    end

    def found_x_months_from_next_dayname
      @constructs << DateConstruct.new(date: @curdate.next(@day_index).add_months(@length), comp_start: @pos, comp_end: @pos += 4, found_in: __method__)
    end

    def match_x_months_from_tomorrow
      @components[@pos + 3] == 'tomorrow'       # if "x months from tomorrow"
    end

    def found_x_months_from_tomorrow
      @constructs << DateConstruct.new(date: @curdate.add_days(1).add_months(@length), comp_start: @pos, comp_end: @pos += 3, found_in: __method__)
    end

    def match_x_months_from_now
      @components[@pos + 3] =~ /\b(today)|(now)\b/    # if "x months from today"
    end

    def found_x_months_from_now
      @constructs << DateConstruct.new(date: @curdate.add_months(@length), comp_start: @pos, comp_end: @pos += 3, found_in: __method__)
    end

    def match_x_months_from_yesterday
      @components[@pos + 3] == 'yesterday'    # "x months from yesterday"
    end

    def found_x_months_from_yesterday
      @constructs << DateConstruct.new(date: @curdate.sub_days(1).add_months(@length), comp_start: @pos, comp_end: @pos += 3, found_in: __method__)
    end

    def match_x_days_from
      ConstructFinder.digits_only?(@components[@pos]) && @components[@pos + 1] =~ /^days?$/ && @components[@pos + 2] == 'from' && @length = @components[@pos].to_i     # 3 days from
    end

    def match_x_days_from_now
      @components[@pos + 3] =~ /\b(now)|(today)\b/           # 3 days from today; 3 days from now
    end

    def found_x_days_from_now
      @constructs << DateConstruct.new(date: @curdate.add_days(@length), comp_start: @pos, comp_end: @pos += 3, found_in: __method__)
    end

    def match_x_days_from_dayname
      @day_index = ZDate.days_of_week.index(@components[@pos + 3])    # 3 days from monday, why would someone do this?
    end

    def found_x_days_from_dayname
      @constructs << DateConstruct.new(date: @curdate.this(@day_index).add_days(@length), comp_start: @pos, comp_end: @pos += 3, found_in: __method__)
    end

    def match_x_dayname_from
      ConstructFinder.digits_only?(@components[@pos]) && (@day_index = ZDate.days_of_week.index(@components[@pos + 1])) && @components[@pos + 2] == 'from' && @length = @components[@pos].to_i    # "2 tuesdays from"
    end

    def match_x_dayname_from_now
      @components[@pos + 3] =~ /\b(today)|(now)\b/     # if "2 tuesdays from now"
    end

    def found_x_dayname_from_now
      # this isn't exactly intuitive.  If someone says "two tuesday from now" and it is tuesday, they mean "in two weeks."  If it is not tuesday, they mean "next tuesday"
      d = (@days_index == @curdate.dayindex) ? @curdate.add_weeks(@length) : @curdate.x_weeks_from_day(@length - 1, @day_index)
      @constructs << DateConstruct.new(date: d, comp_start: @pos, comp_end: @pos += 3, found_in: __method__)
    end

    def match_x_dayname_from_tomorrow
      @components[@pos + 3] == 'tomorrow'
    end

    def found_x_dayname_from_tomorrow
      # If someone says "two tuesday from tomorrow" and tomorrow is tuesday, they mean "two weeks from tomorrow."  If it is not tuesday, this person does not make sense, but we can interpet it as "next tuesday"
      tomorrow_index = (@curdate.dayindex + 1) % 7
      d = (@days_index == tomorrow_index) ? @curdate.add_days(1).add_weeks(@length) : @curdate.x_weeks_from_day(@length - 1, @day_index)
      @constructs << DateConstruct.new(date: d, comp_start: @pos, comp_end: @pos += 3, found_in: __method__)
    end

    def match_x_dayname_from_yesterday
      @components[@pos + 3] == 'yesterday'
    end

    def found_x_dayname_from_yesterday
      # If someone says "two tuesday from yesterday" and yesterday was tuesday, they mean "two weeks from yesterday."  If it is not tuesday, this person does not make sense, but we can interpet it as "next tuesday"
      yesterday_index = (@curdate.dayindex == 0 ? 6 : @curdate.dayindex - 1)
      d = (@days_index == yesterday_index) ? @curdate.sub_days(1).add_weeks(@length) : @curdate.x_weeks_from_day(@length - 1, @day_index)
      @constructs << DateConstruct.new(date: d, comp_start: @pos, comp_end: @pos += 3, found_in: __method__)
    end

    def match_x_dayname_from_this
      @components[@pos + 3] == 'this'    #  "two tuesdays from this"
    end

    def found_x_dayname_from_this
      dc = DateConstruct.new(date: @curdate.this(@day_index).add_weeks(@length), comp_start: @pos, found_in: __method__)
      if @components[@post + 4] == 'one' || ZDate.days_of_week.index(@components[@pos + 4])    # talk about redundant (2 tuesdays from this one, 2 tuesdays from this tuesday)
        dc.comp_end = @pos += 4
      else
        dc.comp_end = @pos += 3
      end
      @constructs << dc
    end

    def match_x_dayname_from_next
      @components[@pos + 3] == 'next'    #  "two tuesdays from next"
    end

    def found_x_dayname_from_next
      dc = DateConstruct.new(date: @curdate.next(@day_index).add_weeks(@length), comp_start: @pos, found_in: __method__)
      if @components[@post + 4] == 'one' || ZDate.days_of_week.index(@components[@pos + 4])    # talk about redundant (2 tuesdays from next one, 2 tuesdays from next tuesday)
        dc.comp_end = @pos += 4
      else
        dc.comp_end = @pos += 3
      end
      @constructs << dc
    end

    def match_x_minutes_from_now
      ConstructFinder.digits_only?(@components[@pos]) && @components[@pos + 1] =~ /minutes?/ && @components[@pos + 2] == 'from' && @components[@pos + 3] =~ /^(today|now)$/ && @length = @components[@pos].to_i
    end

    def found_x_minutes_from_now
      date = nil  # define out of scope of block
      time = @curtime.add_minutes(@length) { |days_to_increment| date = @curdate.add_days(days_to_increment) }
      @constructs << DateConstruct.new(date: date, comp_start: @pos, comp_end: @pos + 4, found_in: __method__)
      @constructs << TimeConstruct.new(time: time, comp_start: @pos, comp_end: @pos += 4, found_in: __method__)
    end

    def match_x_hours_from_now
      ConstructFinder.digits_only?(@components[@pos]) && @components[@pos + 1] =~ /hours?/ && @components[@pos + 2] == 'from' && @components[@pos + 3] =~ /^(today|now)$/ && @length = @components[@pos].to_i
    end

    def found_x_hours_from_now
      date = nil
      time = @curtime.add_hours(@length) { |days_to_increment| date = @curdate.add_days(days_to_increment) }
      @constructs << DateConstruct.new(date: date, comp_start: @pos, comp_end: @pos + 4, found_in: __method__)
      @constructs << TimeConstruct.new(time: time, comp_start: @pos, comp_end: @pos += 4, found_in: __method__)
    end

    def match_ordinal_dayname
      @components[@pos] =~ /(1st|2nd|3rd|4th|5th)/ && (@day_index = ZDate.days_of_week.index(@components[@pos + 1])) && @week_num = @components[@pos].to_i     # last saturday
    end

    def match_ordinal_dayname_this_month
      @components[@pos + 2] == 'this' && @components[@pos + 3] == 'month'                  # last saturday this month
    end

    def found_ordinal_dayname_this_month
      @constructs << DateConstruct.new(date: @curdate.ordinal_dayindex(@week_num, @day_index), comp_start: @pos, comp_end: @pos += 3, found_in: __method__)
    end

    def match_ordinal_dayname_next_month
      @components[@pos + 2] == 'next' && @components[@pos + 3] == 'month'        # 1st monday next month
    end

    def found_ordinal_dayname_next_month
      @constructs << DateConstruct.new(date: @curdate.add_months(1).ordinal_dayindex(@week_num, @day_index), comp_start: @pos, comp_end: @pos += 3, found_in: __method__)
    end

    def match_ordinal_dayname_monthname
      @month_index = ZDate.months_of_year.index(@components[@pos + 2])         # second friday december
    end

    def found_ordinal_dayname_monthname
      @constructs << DateConstruct.new(date: @curdate.jump_to_month(@month_index + 1).ordinal_dayindex(@week_num, @day_index), comp_start: @pos, comp_end: @pos += 2, found_in: __method__)
    end

    def match_ordinal_this_month
      @components[@pos] =~ /(0?[1-9]|[12][0-9]|3[01])(st|nd|rd|th)/ && @components[@pos + 1] == 'this' && @components[@pos + 2] = 'month' && @length = @components[@pos].to_i      # 28th this month
    end

    def match_ordinal_next_month
      @components[@pos] =~ /(0?[1-9]|[12][0-9]|3[01])(st|nd|rd|th)/ && @components[@pos + 1] == 'next' && @components[@pos + 2] = 'month' && @length = @components[@pos].to_i      # 28th next month
    end

    def found_ordinal_this_month
      if @curdate.day > @length
        # e.g. it is the 30th of the month and a user types "1st of the month", they mean "first of next month"
        date = @curdate.add_months(1).beginning_of_month.add_days(@length - 1)
      else
        date = @curdate.beginning_of_month.add_days(@length - 1)
      end
      @constructs << DateConstruct.new(date: date, comp_start: @pos, comp_end: @pos += 2, found_in: __method__)
    end

    def found_ordinal_next_month
      @constructs << DateConstruct.new(date: @curdate.add_months(1).beginning_of_month.add_days(@length - 1), comp_start: @pos, comp_end: @pos += 2, found_in: __method__)
    end

    def match_first_day
      @components[@pos] == '1st' && @components[@pos + 1] == 'day'     # 1st day
    end

    def match_first_day_this_month
      @components[@pos + 2] == 'this' && @components[@pos + 3] == 'month'                  # 1st day this month
    end

    def found_first_day_this_month
      @constructs << DateConstruct.new(date: @curdate.beginning_of_month, comp_start: @pos, comp_end: @pos += 3, found_in: __method__)
    end

    def match_first_day_next_month
      @components[@pos + 2] == 'next' && @components[@pos + 3] == 'month'        # 1st day next month
    end

    def found_first_day_next_month
      @constructs << DateConstruct.new(date: @curdate.add_months(1).beginning_of_month, comp_start: @pos, comp_end: @pos += 3, found_in: __method__)
    end

    def match_first_day_monthname
      @month_index = ZDate.months_of_year.index(@components[@pos + 2])         # 1st day december
    end

    def found_first_day_monthname
      @constructs << DateConstruct.new(date: @curdate.jump_to_month(@month_index + 1), comp_start: @pos, comp_end: @pos += 2, found_in: __method__)
    end

    def match_last_day
      @components[@pos] == 'last' && @components[@pos + 1] == 'day'     # last day
    end

    def match_last_day_this_month
      @components[@pos + 2] == 'this' && @components[@pos + 3] == 'month'                  # 1st day this month
    end

    def found_last_day_this_month
      @constructs << DateConstruct.new(date: @curdate.end_of_month, comp_start: @pos, comp_end: @pos += 3, found_in: __method__)
    end

    def match_last_day_next_month
      @components[@pos + 2] == 'next' && @components[@pos + 3] == 'month'        # 1st day next month
    end

    def found_last_day_next_month
      @constructs << DateConstruct.new(date: @curdate.add_months(1).end_of_month, comp_start: @pos, comp_end: @pos += 3, found_in: __method__)
    end

    def match_last_day_monthname
      @month_index = ZDate.months_of_year.index(@components[@pos + 2])         # 1st day december
    end

    def found_last_day_monthname
      @constructs << DateConstruct.new(date: @curdate.jump_to_month(@month_index + 1).end_of_month, comp_start: @pos, comp_end: @pos += 2, found_in: __method__)
    end

    def match_at
      @components[@pos] == 'at'
    end

    def match_at_time
      @components[@pos + 1] && @time1 = ZTime.interpret(@components[@pos + 1])
    end

    def match_at_time_through_time
      @components[@pos + 2] =~ /^(to|until|through)$/ && @components[@pos + 3] && @time2 = ZTime.interpret(@components[@pos + 3])
    end

    def found_at_time_through_time
      @constructs << TimeSpanConstruct.new(start_time: @time1, end_time: @time2, comp_start: @pos, comp_end: @pos += 3, found_in: __method__)
    end

    def found_at_time
      @constructs << TimeConstruct.new(time: @time1, comp_start: @pos, comp_end: @pos += 1, found_in: __method__)
    end

    def match_all_day
      @components[@pos] == 'all' && @components[@pos + 1] == 'day'      # all day
    end

    def found_all_day
      @constructs << NullConstruct.new(comp_start: @pos, comp_end: @pos += 1, found_in: __method__)
    end

    def match_tomorrow
      @components[@pos] == 'tomorrow'
    end

    def match_tomorrow_through
      @components[@pos + 1] == 'until' || @components[@pos + 1] == 'to' || @components[@pos + 1] == 'through'    # "tomorrow through"
    end

    def match_tomorrow_through_dayname
      @day_index = ZDate.days_of_week.index(@components[@pos + 2])       # tomorrow through thursday
    end

    def found_tomorrow_through_dayname
      @constructs << DateSpanConstruct.new(start_date: @curdate.add_days(1), end_date: @curdate.add_days(1).this(@day_index), comp_start: @pos, comp_end: @pos += 2, found_in: __method__)
    end

    def match_tomorrow_through_date
      @date1 = ZDate.interpret(@components[@pos + 2], @curdate)       # tomorrow until 9/21
    end

    def found_tomorrow_through_date
      @constructs << DateSpanConstruct.new(start_date: @curdate.add_days(1), end_date: @date1, comp_start: @pos, comp_end: @pos += 2, found_in: __method__)
    end

    def found_tomorrow
      @constructs << DateConstruct.new(date: @curdate.add_days(1), comp_start: @pos, comp_end: @pos, found_in: __method__)
    end

    def match_now
      @components[@pos] == 'today' || @components[@pos] == 'now'
    end

    def match_now_through
      @components[@pos + 1] == 'until' || @components[@pos + 1] == 'to' || @components[@pos + 1] == 'through'   # "today through"
    end

    def match_now_through_dayname
      @day_index = ZDate.days_of_week.index(@components[@pos + 2])     # today through thursday
    end

    def found_now_through_dayname
      @constructs << DateSpanConstruct.new(start_date: @curdate, end_date: @curdate.this(@day_index), comp_start: @pos, comp_end: @pos += 2, found_in: __method__)
    end

    # redundant!! preprocess this out of here!
    def match_now_through_following_dayname
      @components[@pos + 2] =~ /following|this/ && @day_index = ZDate.days_of_week.index(@components[@pos + 3])    # today through following friday
    end

    # redundant!! preprocess this out of here!
    def found_now_through_following_dayname
      @constructs << DateSpanConstruct.new(start_date: @curdate, end_date: @curdate.this(@day_index), comp_start: @pos, comp_end: @pos += 3, found_in: __method__)
    end

    def match_now_through_date
      @date1 = ZDate.interpret(@components[@pos + 2], @curdate)       # now until 9/21
    end

    def found_now_through_date
      @constructs << DateSpanConstruct.new(start_date: @curdate, end_date: @date1, comp_start: @pos, comp_end: @pos += 2, found_in: __method__)
    end

    def match_now_through_tomorrow
      @components[@pos + 2] == 'tomorrow'
    end

    def found_now_through_tomorrow
      @constructs << DateSpanConstruct.new(start_date: @curdate, end_date: @curdate.add_days(1), comp_start: @pos, comp_end: @pos += 2, found_in: __method__)
    end

    def match_now_through_next_dayname
      @components[@pos + 2] == 'next' && @day_index = ZDate.days_of_week.index(@components[@pos + 3])     # Today through next friday
    end

    def found_now_through_next_dayname
      @constructs << DateSpanConstruct.new(start_date: @curdate, end_date: @curdate.next(@day_index), comp_start: @pos, comp_end: @pos += 3, found_in: __method__)
    end

    def found_now
      @constructs << DateConstruct.new(date: @curdate, comp_start: @pos, comp_end: @pos, found_in: __method__)
    end

    def match_dayname
      @day_index = ZDate.days_of_week.index(@components[@pos])
    end

    def match_dayname_the_ordinal
      @components[@pos + 1] == 'the' && @date1 = ZDate.interpret(@components[@pos + 2], @curdate)    # if "tue the 23rd"
    end

    def found_dayname_the_ordinal
      # user may have specified "monday the 2nd" while in the previous month, so first check if dayname matches date.dayname, if it doesn't increment by a month and check again
      if @date1.dayname == @components[@pos] || ((tmp = @date1.add_months(1)) && tmp.dayname == @components[@pos] && @date1 = tmp)
        @constructs << DateConstruct.new(date: @date1, comp_start: @pos, comp_end: @pos += 2, found_in: __method__)
      end
    end

    def match_dayname_x_weeks_from_this
      @components[@pos + 1] && ConstructFinder.digits_only?(@components[@pos + 1]) && @components[@pos + 2] =~ /\bweeks?\b/ && @components[@pos + 3] =~ /\b(from)|(after)/ && @components[@pos + 4] == 'this' && @length = @components[@pos + 1]           # "monday two weeks from this
    end

    def found_dayname_x_weeks_from_this
      dc = DateConstruct.new(date: @curdate.this(@dayindex).add_weeks(@length), comp_start: @pos, found_in: __method__)
      if ZDate.days_of_week.include?(@components[@pos + 5])  # redundant
        dc.comp_end = @pos += 5
      else
        dc.comp_end = @pos += 4
      end
      @constructs << dc
    end

    def match_dayname_x_weeks_from_next
      @components[@pos + 1] && ConstructFinder.digits_only?(@components[@pos + 1]) && @components[@pos + 2] =~ /\bweeks?\b/ && @components[@pos + 3] =~ /\b(from)|(after)/ && @components[@pos + 4] == 'next' && @length = @components[@pos + 1]           # "monday two weeks from this
    end

    def found_dayname_x_weeks_from_next
      dc = DateConstruct.new(date: @curdate.next(@dayindex).add_weeks(@length), comp_start: @pos, found_in: __method__)
      if ZDate.days_of_week.include?(@components[@pos + 5])  # redundant
        dc.comp_end = @pos += 5
      else
        dc.comp_end = @pos += 4
      end
      @constructs << h
    end

    # redundant, same as found_this_dayname
    def found_dayname
      day_to_add = @curdate.this(@day_index)
      @constructs << DateConstruct.new(date: day_to_add, comp_start: @pos, comp_end: @pos, found_in: __method__)
      while @components[@pos + 1] && @day_index = ZDate.days_of_week.index(@components[@pos + 1])
        # note @pos gets incremented here:
        @constructs << DateConstruct.new(date: day_to_add = day_to_add.this(@day_index), comp_start: @pos + 1, comp_end: @pos += 1, found_in: __method__)
      end
    end

    def match_through_monthname
      @components[@pos] == 'through' && @month_index = ZDate.months_of_year.index(@components[@pos + 1])
    end

    def found_through_monthname
      # this is really a wrapper, we don't know when the start date is, so make sure @constructs gets wrapper first, as date constructs always have to appear after wrapper
      @constructs << WrapperConstruct.new(wrapper_type: 1, comp_start: @pos, comp_end: @pos + 1, found_in: __method__)
      @constructs << DateConstruct.new(date: @curdate.jump_to_month(@month_index + 1).sub_days(1), comp_start: @pos, comp_end: @pos += 1, found_in: __method__)
    end

    def match_monthname
      # note it is important that all other uses of monthname come after indicating words such as "the third day of december"; otherwise they will be converted here
      @month_index = ZDate.months_of_year.index(@components[@pos])
    end

    def found_monthname
      sd = @curdate.jump_to_month(@month_index + 1)
      ed = sd.end_of_month
      @constructs << DateSpanConstruct.new(start_date: sd, end_date: ed, comp_start: @pos, comp_end: @pos, found_in: __method__)
    end

    def match_start
      @components[@pos] == 'start'
    end

    def found_start
      # wrapper_type 0 is a start wrapper
      @constructs << WrapperConstruct.new(wrapper_type: 0, comp_start: @pos, comp_end: @pos, found_in: __method__)
    end

    def match_through
      @components[@pos] == 'through'
    end

    def found_through
      # wrapper_type 1 is an end wrapper
      @constructs << WrapperConstruct.new(wrapper_type: 1, comp_start: @pos, comp_end: @pos, found_in: __method__)
    end

    def match_time
      @time1 = ZTime.interpret(@components[@pos])
    end

    def match_time_through_time
      @components[@pos + 1] =~ /^(to|through)$/ && @time2 = ZTime.interpret(@components[@pos + 2])
    end

    def found_time_through_time
      @constructs << TimeSpanConstruct.new(start_time: @time1, end_time: @time2, comp_start: @pos, comp_end: @pos += 2, found_in: __method__)
    end

    def found_time
      @constructs << TimeConstruct.new(time: @time1, comp_start: @pos, comp_end: @pos, found_in: __method__)
    end

    def match_date
      @date1 = ZDate.interpret(@components[@pos], @curdate)
    end

    def match_date_through_date
      @components[@pos + 1] =~ /^(through|to|until)$/ && @date2 = ZDate.interpret(@components[@pos + 2], @curdate)
    end

    def found_date_through_date
      @constructs << DateSpanConstruct.new(start_date: @date1, end_date: @date2, comp_start: @pos, comp_end: @pos += 2, found_in: __method__)
    end

    def found_date
      @constructs << DateConstruct.new(date: @date1, comp_start: @pos, comp_end: @pos, found_in: __method__)
    end

    class << self
      def digits_only?(str)
        str =~ /^\d+$/ # no characters other than digits
      end

      # valid hour, 24hour, and minute could use some cleaning
      def ordinal_only?(str)
        str =~ %r{^(0?[1-9]|[12][0-9]|3[01])(?:st|nd|rd|th)?$}
      end
    end
  end # END class ConstructFinder
end