public class DummyHandler implements IHandler {
  public Span handle(List<Token> tokens, Options options) {
    return null;
  }
} public class Handler {
  private static Map<Handler.HandlerType, List<Handler>> _definitions;

  public static enum HandlerType {
    TIME, DATE, ANCHOR, ARROW, NARROW
  }

  private HandlerPattern[] _patterns;
  private IHandler _handler;
  private boolean _compatible;

  public Handler(IHandler handler, HandlerPattern... patterns) {
    this(handler, true, patterns);
  }
  
  public Handler(IHandler handler, boolean compatible, HandlerPattern... patterns) {
    _handler = handler;
    _compatible = compatible;
    _patterns = patterns;
  }
  
  public boolean isCompatible(Options options) {
    return !options.isCompatibilityMode() || _compatible;
  }

  public IHandler getHandler() {
    return _handler;
  }

  public boolean match(List<Token> tokens, Map<Handler.HandlerType, List<Handler>> definitions) {
    // System.out.println("Handler.match: " + this);
    int tokenIndex = 0;
    for (HandlerPattern pattern : _patterns) {
      boolean optional = pattern.isOptional();
      if (pattern instanceof TagPattern) {
        boolean match = (tokenIndex < tokens.size() && tokens.get(tokenIndex).getTags(((TagPattern) pattern).getTagClass()).size() > 0);
        // System.out.println("Handler.match:   " + pattern + "=" + match);
        if (!match && !optional) {
          return false;
        }
        if (match) {
          tokenIndex++;
        }
        // next if !match && optional ?
      }
      else if (pattern instanceof HandlerTypePattern) {
        if (optional && tokenIndex == tokens.size()) {
          return true;
        }
        List<Handler> subHandlers = definitions.get(((HandlerTypePattern) pattern).getType());
        for (Handler subHandler : subHandlers) {
          if (subHandler.match(tokens.subList(tokenIndex, tokens.size()), definitions)) {
            return true;
          }
        }
        return false;
      }
    }
    if (tokenIndex != tokens.size()) {
      return false;
    }
    return true;
  }

  @Override
  public String toString() {
    return "[Handler: " + _handler + "]";
  }

  public static synchronized Map<Handler.HandlerType, List<Handler>> definitions() {
    if (_definitions == null) {
      Map<Handler.HandlerType, List<Handler>> definitions = new HashMap<Handler.HandlerType, List<Handler>>();

      List<Handler> timeHandlers = new LinkedList<Handler>();
      timeHandlers.add(new Handler(null, new TagPattern(RepeaterTime.class), new TagPattern(RepeaterDayPortion.class, true)));
      definitions.put(Handler.HandlerType.TIME, timeHandlers);

      List<Handler> dateHandlers = new LinkedList<Handler>();
      dateHandlers.add(new Handler(new RdnRmnSdTTzSyHandler(), new TagPattern(RepeaterDayName.class), new TagPattern(RepeaterMonthName.class), new TagPattern(ScalarDay.class), new TagPattern(RepeaterTime.class), new TagPattern(TimeZone.class), new TagPattern(ScalarYear.class)));
      // DIFF: We add an optional comma to MDY
      dateHandlers.add(new Handler(new RmnSdSyHandler(), new TagPattern(RepeaterMonthName.class), new TagPattern(ScalarDay.class), new TagPattern(SeparatorComma.class, true), new TagPattern(ScalarYear.class)));
      dateHandlers.add(new Handler(new RmnSdSyHandler(), new TagPattern(RepeaterMonthName.class), new TagPattern(ScalarDay.class), new TagPattern(ScalarYear.class), new TagPattern(SeparatorAt.class, true), new HandlerTypePattern(Handler.HandlerType.TIME, true)));
      dateHandlers.add(new Handler(new RmnSdHandler(), new TagPattern(RepeaterMonthName.class), new TagPattern(ScalarDay.class), new TagPattern(SeparatorAt.class, true), new HandlerTypePattern(Handler.HandlerType.TIME, true)));
      dateHandlers.add(new Handler(new RmnOdHandler(), new TagPattern(RepeaterMonthName.class), new TagPattern(OrdinalDay.class), new TagPattern(SeparatorAt.class, true), new HandlerTypePattern(Handler.HandlerType.TIME, true)));
      dateHandlers.add(new Handler(new RmnSyHandler(), new TagPattern(RepeaterMonthName.class), new TagPattern(ScalarYear.class)));
      dateHandlers.add(new Handler(new SdRmnSyHandler(), new TagPattern(ScalarDay.class), new TagPattern(RepeaterMonthName.class), new TagPattern(ScalarYear.class), new TagPattern(SeparatorAt.class, true), new HandlerTypePattern(Handler.HandlerType.TIME, true)));
      dateHandlers.add(new Handler(new SmSdSyHandler(), new TagPattern(ScalarMonth.class), new TagPattern(SeparatorSlashOrDash.class), new TagPattern(ScalarDay.class), new TagPattern(SeparatorSlashOrDash.class), new TagPattern(ScalarYear.class), new TagPattern(SeparatorAt.class, true), new HandlerTypePattern(Handler.HandlerType.TIME, true)));
      dateHandlers.add(new Handler(new SdSmSyHandler(), new TagPattern(ScalarDay.class), new TagPattern(SeparatorSlashOrDash.class), new TagPattern(ScalarMonth.class), new TagPattern(SeparatorSlashOrDash.class), new TagPattern(ScalarYear.class), new TagPattern(SeparatorAt.class, true), new HandlerTypePattern(Handler.HandlerType.TIME, true)));
      dateHandlers.add(new Handler(new SySmSdHandler(), new TagPattern(ScalarYear.class), new TagPattern(SeparatorSlashOrDash.class), new TagPattern(ScalarMonth.class), new TagPattern(SeparatorSlashOrDash.class), new TagPattern(ScalarDay.class), new TagPattern(SeparatorAt.class, true), new HandlerTypePattern(Handler.HandlerType.TIME, true)));
      // DIFF: We make 05/06 interpret as month/day before month/year
      dateHandlers.add(new Handler(new SmSdHandler(), false, new TagPattern(ScalarMonth.class), new TagPattern(SeparatorSlashOrDash.class), new TagPattern(ScalarDay.class)));
      dateHandlers.add(new Handler(new SmSyHandler(), new TagPattern(ScalarMonth.class), new TagPattern(SeparatorSlashOrDash.class), new TagPattern(ScalarYear.class)));
      definitions.put(Handler.HandlerType.DATE, dateHandlers);

      // tonight at 7pm
      List<Handler> anchorHandlers = new LinkedList<Handler>();
      anchorHandlers.add(new Handler(new RHandler(), new TagPattern(Grabber.class, true), new TagPattern(Repeater.class), new TagPattern(SeparatorAt.class, true), new TagPattern(Repeater.class, true), new TagPattern(Repeater.class, true)));
      anchorHandlers.add(new Handler(new RHandler(), new TagPattern(Grabber.class, true), new TagPattern(Repeater.class), new TagPattern(Repeater.class), new TagPattern(SeparatorAt.class, true), new TagPattern(Repeater.class, true), new TagPattern(Repeater.class, true)));
      anchorHandlers.add(new Handler(new RGRHandler(), new TagPattern(Repeater.class), new TagPattern(Grabber.class), new TagPattern(Repeater.class)));
      definitions.put(Handler.HandlerType.ANCHOR, anchorHandlers);

      // 3 weeks from now, in 2 months
      List<Handler> arrowHandlers = new LinkedList<Handler>();
      arrowHandlers.add(new Handler(new SRPHandler(), new TagPattern(Scalar.class), new TagPattern(Repeater.class), new TagPattern(Pointer.class)));
      arrowHandlers.add(new Handler(new PSRHandler(), new TagPattern(Pointer.class), new TagPattern(Scalar.class), new TagPattern(Repeater.class)));
      arrowHandlers.add(new Handler(new SRPAHandler(), new TagPattern(Scalar.class), new TagPattern(Repeater.class), new TagPattern(Pointer.class), new HandlerTypePattern(Handler.HandlerType.ANCHOR)));
      definitions.put(Handler.HandlerType.ARROW, arrowHandlers);

      // 3rd week in march
      List<Handler> narrowHandlers = new LinkedList<Handler>();
      narrowHandlers.add(new Handler(new ORSRHandler(), new TagPattern(Ordinal.class), new TagPattern(Repeater.class), new TagPattern(SeparatorIn.class), new TagPattern(Repeater.class)));
      narrowHandlers.add(new Handler(new ORGRHandler(), new TagPattern(Ordinal.class), new TagPattern(Repeater.class), new TagPattern(Grabber.class), new TagPattern(Repeater.class)));
      definitions.put(Handler.HandlerType.NARROW, narrowHandlers);
      _definitions = definitions;
    }
    return _definitions;
  }

  public static Span tokensToSpan(List<Token> tokens, Options options) {
    if (options.isDebug()) {
      System.out.println("Chronic.tokensToSpan: " + tokens);
    }

    // maybe it's a specific date
    Map<Handler.HandlerType, List<Handler>> definitions = definitions();
    for (Handler handler : definitions.get(Handler.HandlerType.DATE)) {
      if (handler.isCompatible(options) && handler.match(tokens, definitions)) {
        if (options.isDebug()) {
          System.out.println("Chronic.tokensToSpan: date");
        }
        List<Token> goodTokens = new LinkedList<Token>();
        for (Token token : tokens) {
          if (token.getTag(Separator.class) == null) {
            goodTokens.add(token);
          }
        }
        return handler.getHandler().handle(goodTokens, options);
      }
    }

    // I guess it's not a specific date, maybe it's just an anchor
    for (Handler handler : definitions.get(Handler.HandlerType.ANCHOR)) {
      if (handler.isCompatible(options) && handler.match(tokens, definitions)) {
        if (options.isDebug()) {
          System.out.println("Chronic.tokensToSpan: anchor");
        }
        List<Token> goodTokens = new LinkedList<Token>();
        for (Token token : tokens) {
          if (token.getTag(Separator.class) == null) {
            goodTokens.add(token);
          }
        }
        return handler.getHandler().handle(goodTokens, options);
      }
    }

    // not an anchor, perhaps it's an arrow
    for (Handler handler : definitions.get(Handler.HandlerType.ARROW)) {
      if (handler.isCompatible(options) && handler.match(tokens, definitions)) {
        if (options.isDebug()) {
          System.out.println("Chronic.tokensToSpan: arrow");
        }
        List<Token> goodTokens = new LinkedList<Token>();
        for (Token token : tokens) {
          if (token.getTag(SeparatorAt.class) == null && token.getTag(SeparatorSlashOrDash.class) == null && token.getTag(SeparatorComma.class) == null) {
            goodTokens.add(token);
          }
        }
        return handler.getHandler().handle(goodTokens, options);
      }
    }

    // not an arrow, let's hope it's a narrow
    for (Handler handler : definitions.get(Handler.HandlerType.NARROW)) {
      if (handler.isCompatible(options) && handler.match(tokens, definitions)) {
        if (options.isDebug()) {
          System.out.println("Chronic.tokensToSpan: narrow");
        }
        //List<Token> goodTokens = new LinkedList<Token>();
        //for (Token token : tokens) {
        //if (token.getTag(Separator.class) == null) {
        //  goodTokens.add(token);
        //}
        //}
        return handler.getHandler().handle(tokens, options);
      }
    }

    // I guess you're out of luck!
    if (options.isDebug()) {
      System.out.println("Chronic.tokensToSpan: none");
    }
    return null;
  }

  public static List<Repeater<?>> getRepeaters(List<Token> tokens) {
    List<Repeater<?>> repeaters = new LinkedList<Repeater<?>>();
    for (Token token : tokens) {
      Repeater<?> tag = token.getTag(Repeater.class);
      if (tag != null) {
        repeaters.add(tag);
      }
    }
    Collections.sort(repeaters);
    Collections.reverse(repeaters);
    return repeaters;
  }

  public static Span getAnchor(List<Token> tokens, Options options) {
    Grabber grabber = new Grabber(Grabber.Relative.THIS);
    Pointer.PointerType pointer = Pointer.PointerType.FUTURE;

    List<Repeater<?>> repeaters = getRepeaters(tokens);
    for (int i = 0; i < repeaters.size(); i++) {
      tokens.remove(tokens.size() - 1);
    }

    if (!tokens.isEmpty() && tokens.get(0).getTag(Grabber.class) != null) {
      grabber = tokens.get(0).getTag(Grabber.class);
      tokens.remove(tokens.size() - 1);
    }

    Repeater<?> head = repeaters.remove(0);
    head.setStart((Calendar) options.getNow().clone());

    Span outerSpan;
    Grabber.Relative grabberType = grabber.getType();
    if (grabberType == Grabber.Relative.LAST) {
      outerSpan = head.nextSpan(Pointer.PointerType.PAST);
    }
    else if (grabberType == Grabber.Relative.THIS) {
      if (repeaters.size() > 0) {
        outerSpan = head.thisSpan(PointerType.NONE);
      }
      else {
        outerSpan = head.thisSpan(options.getContext());
      }
    }
    else if (grabberType == Grabber.Relative.NEXT) {
      outerSpan = head.nextSpan(Pointer.PointerType.FUTURE);
    }
    else {
      throw new IllegalArgumentException("Invalid grabber type " + grabberType + ".");
    }

    if (options.isDebug()) {
      System.out.println("Chronic.getAnchor: outerSpan = " + outerSpan + "; repeaters = " + repeaters);
    }

    Span anchor = findWithin(repeaters, outerSpan, pointer, options);
    return anchor;
  }

  public static Span dayOrTime(Calendar dayStart, List<Token> timeTokens, Options options) {
    Span outerSpan = new Span(dayStart, Time.cloneAndAdd(dayStart, Calendar.DAY_OF_MONTH, 1));
    if (!timeTokens.isEmpty()) {

//      /** SUPER HACK MODE FOR TIMES **/
//      Tag<?> dayPortionTag = null;
//      Tag<?> timeTag = null;
//      for (Token token : timeTokens) {
//        Tag<?> tempDayPortionTag = token.getTag(RepeaterDayPortion.class);
//        if (tempDayPortionTag != null) {
//          dayPortionTag = tempDayPortionTag;
//        }
//
//        Tag<?> tempTimeTag = token.getTag(RepeaterTime.class);
//        if (tempTimeTag != null) {
//          timeTag = tempTimeTag;
//        }
//      }
//      
//      if (timeTag != null && dayPortionTag != null) {
//        Tick tick = (Tick)timeTag.getType();
//        RepeaterDayPortion.DayPortion dayPortion = (RepeaterDayPortion.DayPortion)dayPortionTag.getType();
//        if (tick.intValue() <= (RepeaterDay.DAY_SECONDS / 2)) {
//          if (dayPortion == RepeaterDayPortion.DayPortion.PM) {
//            if (tick.intValue() == (12 * 60 * 60)) {
//              Calendar exactTime = Time.cloneAndAdd(dayStart, Calendar.SECOND, tick.intValue());
//              return new Span(exactTime, exactTime);
//            }
//            Calendar exactTime = Time.cloneAndAdd(dayStart, Calendar.SECOND, tick.intValue() + RepeaterDay.DAY_SECONDS / 2);
//            return new Span(exactTime, exactTime);
//          }
//          else if (dayPortion == RepeaterDayPortion.DayPortion.AM) {
//            if (tick.intValue() == (12 * 60 * 60)) {
//              Calendar exactTime = dayStart;
//              return new Span(exactTime, exactTime);
//            }
//            Calendar exactTime = Time.cloneAndAdd(dayStart, Calendar.SECOND, tick.intValue());
//            return new Span(exactTime, exactTime);
//          }
//        }
//      }
//      /** SUPER HACK MODE FOR TIMES **/

      options.setNow(outerSpan.getBeginCalendar());
      Span time = getAnchor(dealiasAndDisambiguateTimes(timeTokens, options), options);
      return time;
    }
    return outerSpan;
  }

  /**
   * Recursively finds repeaters within other repeaters.
   * Returns a Span representing the innermost time span
   * or nil if no repeater union could be found
   */
  public static Span findWithin(List<Repeater<?>> tags, Span span, Pointer.PointerType pointer, Options options) {
    if (options.isDebug()) {
      System.out.println("Chronic.findWithin: " + tags + " in " + span);
    }
    if (tags.isEmpty()) {
      return span;
    }
    Repeater<?> head = tags.get(0);
    List<Repeater<?>> rest = (tags.size() > 1) ? tags.subList(1, tags.size()) : new LinkedList<Repeater<?>>();
    head.setStart((pointer == Pointer.PointerType.FUTURE) ? span.getBeginCalendar() : span.getEndCalendar());
    Span h = head.thisSpan(PointerType.NONE);

    if (span.contains(h.getBegin()) || span.contains(h.getEnd())) {
      return findWithin(rest, h, pointer, options);
    }
    return null;
  }

  @SuppressWarnings("unchecked")
  public static List<Token> dealiasAndDisambiguateTimes(List<Token> tokens, Options options) {
    // handle aliases of am/pm
    // 5:00 in the morning => 5:00 am
    // 7:00 in the evening => 7:00 pm

    int dayPortionIndex = -1;
    int tokenSize = tokens.size();
    for (int i = 0; dayPortionIndex == -1 && i < tokenSize; i++) {
      Token t = tokens.get(i);
      if (t.getTag(RepeaterDayPortion.class) != null) {
        dayPortionIndex = i;
      }
    }

    int timeIndex = -1;
    for (int i = 0; timeIndex == -1 && i < tokenSize; i++) {
      Token t = tokens.get(i);
      if (t.getTag(RepeaterTime.class) != null) {
        timeIndex = i;
      }
    }

    if (dayPortionIndex != -1 && timeIndex != -1) {
      Token t1 = tokens.get(dayPortionIndex);
      Tag<RepeaterDayPortion<?>> t1Tag = t1.getTag(RepeaterDayPortion.class);

      Object t1TagType = t1Tag.getType();
      if (RepeaterDayPortion.DayPortion.MORNING.equals(t1TagType)) {
        if (options.isDebug()) {
          System.out.println("Chronic.dealiasAndDisambiguateTimes: morning->am");
        }
        t1.untag(RepeaterDayPortion.class);
        t1.tag(new EnumRepeaterDayPortion(RepeaterDayPortion.DayPortion.AM));
      }
      else if (RepeaterDayPortion.DayPortion.AFTERNOON.equals(t1TagType) || RepeaterDayPortion.DayPortion.EVENING.equals(t1TagType) || RepeaterDayPortion.DayPortion.NIGHT.equals(t1TagType)) {
        if (options.isDebug()) {
          System.out.println("Chronic.dealiasAndDisambiguateTimes: " + t1TagType + "->pm");
        }
        t1.untag(RepeaterDayPortion.class);
        t1.tag(new EnumRepeaterDayPortion(RepeaterDayPortion.DayPortion.PM));
      }
    }

//    int tokenSize = tokens.size();
//    for (int i = 0; i < tokenSize; i++) {
//      Token t0 = tokens.get(i);
//      if (i < tokenSize - 1) {
//        Token t1 = tokens.get(i + 1);
//        RepeaterDayPortion<?> t1Tag = t1.getTag(RepeaterDayPortion.class);
//        if (t1Tag != null && t0.getTag(RepeaterTime.class) != null) {
//          if (t1Tag.getType() == RepeaterDayPortion.DayPortion.MORNING) {
//            t1.untag(RepeaterDayPortion.class);
//            t1.tag(new EnumRepeaterDayPortion(RepeaterDayPortion.DayPortion.AM));
//          }
//          else if (t1Tag.getType() == RepeaterDayPortion.DayPortion.AFTERNOON || t1Tag.getType() == RepeaterDayPortion.DayPortion.EVENING || t1Tag.getType() == RepeaterDayPortion.DayPortion.NIGHT) {
//            t1.untag(RepeaterDayPortion.class);
//            t1.tag(new EnumRepeaterDayPortion(RepeaterDayPortion.DayPortion.PM));
//          }
//        }
//      }
//    }

    // handle ambiguous times if :ambiguous_time_range is specified
    if (options.getAmbiguousTimeRange() != 0) {
      List<Token> ttokens = new LinkedList<Token>();
      for (int i = 0; i < tokenSize; i++) {
        Token t0 = tokens.get(i);
        ttokens.add(t0);
        Token t1 = null;
        if (i < tokenSize - 1) {
          t1 = tokens.get(i + 1);
        }
        if (t0.getTag(RepeaterTime.class) != null && t0.getTag(RepeaterTime.class).getType().isAmbiguous() && (t1 == null || t1.getTag(RepeaterDayPortion.class) == null)) {
          Token distoken = new Token("disambiguator");
          distoken.tag(new IntegerRepeaterDayPortion(Integer.valueOf(options.getAmbiguousTimeRange())));
          ttokens.add(distoken);
        }
      }
      tokens = ttokens;
    }

    if (options.isDebug()) {
      System.out.println("Chronic.dealiasAndDisambiguateTimes: " + tokens);
    }

    return tokens;
  }
} public class HandlerTypePattern extends HandlerPattern {
  private Handler.HandlerType _type;

  public HandlerTypePattern(Handler.HandlerType type) {
    this(type, false);
  }
  
  public HandlerTypePattern(Handler.HandlerType type, boolean optional) {
    super(optional);
    _type = type;
  }

  public Handler.HandlerType getType() {
    return _type;
  }
} @SuppressWarnings("unchecked")
public class TagPattern extends HandlerPattern {
  private Class<? extends Tag> _tagClass;

  public TagPattern(Class<? extends Tag> tagClass) {
    this(tagClass, false);
  }
  
  public TagPattern(Class<? extends Tag> tagClass, boolean optional) {
    super(optional);
    _tagClass = tagClass;
  }
  
  public Class<? extends Tag> getTagClass() {
    return _tagClass;
  }
  
  @Override
  public String toString() {
    return "[TagPattern: tagClass = " + _tagClass + "]";
  }
} public class EnumRepeaterDayPortion extends RepeaterDayPortion<RepeaterDayPortion.DayPortion> {
  private static final Range AM_RANGE = new Range(0, 12 * 60 * 60); // 12am-12pm
  private static final Range PM_RANGE = new Range(12 * 60 * 60, 24 * 60 * 60 - 1); // 12pm-12am
  private static final Range MORNING_RANGE = new Range(6 * 60 * 60, 12 * 60 * 60); // 6am-12pm
  private static final Range AFTERNOON_RANGE = new Range(13 * 60 * 60, 17 * 60 * 60); // 1pm-5pm
  private static final Range EVENING_RANGE = new Range(17 * 60 * 60, 20 * 60 * 60); // 5pm-8pm
  private static final Range NIGHT_RANGE = new Range(20 * 60 * 60, 24 * 60 * 60); // 8pm-12pm

  public EnumRepeaterDayPortion(RepeaterDayPortion.DayPortion type) {
    super(type);
  }

  @Override
  protected Range createRange(RepeaterDayPortion.DayPortion type) {
    Range range;
    if (type == RepeaterDayPortion.DayPortion.AM) {
      range = EnumRepeaterDayPortion.AM_RANGE;
    }
    else if (type == RepeaterDayPortion.DayPortion.PM) {
      range = EnumRepeaterDayPortion.PM_RANGE;
    }
    else if (type == RepeaterDayPortion.DayPortion.MORNING) {
      range = EnumRepeaterDayPortion.MORNING_RANGE;
    }
    else if (type == RepeaterDayPortion.DayPortion.AFTERNOON) {
      range = EnumRepeaterDayPortion.AFTERNOON_RANGE;
    }
    else if (type == RepeaterDayPortion.DayPortion.EVENING) {
      range = EnumRepeaterDayPortion.EVENING_RANGE;
    }
    else if (type == RepeaterDayPortion.DayPortion.NIGHT) {
      range = EnumRepeaterDayPortion.NIGHT_RANGE;
    }
    else {
      throw new IllegalArgumentException("Unknown day portion type " + type);
    }
    return range;
  }
  
  @Override
  protected int _getWidth(Range range) {
    int width = (int) range.getWidth();
    return width;
  }
} public abstract class Repeater<T> extends Tag<T> implements Comparable<Repeater<?>> {
  public Repeater(T type) {
    super(type);
  }

  public static List<Token> scan(List<Token> tokens) {
    return Repeater.scan(tokens, new Options());
  }

  public static List<Token> scan(List<Token> tokens, Options options) {
    for (Token token : tokens) {
      Tag<?> t;
      t = RepeaterMonthName.scan(token);
      if (t != null) {
        token.tag(t);
      }
      t = RepeaterDayName.scan(token);
      if (t != null) {
        token.tag(t);
      }
      t = RepeaterDayPortion.scan(token);
      if (t != null) {
        token.tag(t);
      }
      t = RepeaterTime.scan(token, tokens, options);
      if (t != null) {
        token.tag(t);
      }
      t = RepeaterUnit.scan(token);
      if (t != null) {
        token.tag(t);
      }
    }
    return tokens;
  }

  public int compareTo(Repeater<?> other) {
    return Integer.valueOf(getWidth()).compareTo(Integer.valueOf(other.getWidth()));
  }

  /**
   * returns the width (in seconds or months) of this repeatable.
   */
  public abstract int getWidth();

  /** 
   * returns the next occurance of this repeatable.
   */
  public Span nextSpan(Pointer.PointerType pointer) {
    if (getNow() == null) {
      throw new IllegalStateException("Start point must be set before calling #next");
    }
    return _nextSpan(pointer);
  }

  protected abstract Span _nextSpan(Pointer.PointerType pointer);

  public Span thisSpan(Pointer.PointerType pointer) {
    if (getNow() == null) {
      throw new IllegalStateException("Start point must be set before calling #this");
    }
    return _thisSpan(pointer);
  }

  protected abstract Span _thisSpan(Pointer.PointerType pointer);

  public abstract Span getOffset(Span span, int amount, Pointer.PointerType pointer);

  @Override
  public String toString() {
    return "repeater";
  }
} public class RepeaterDayName extends Repeater<RepeaterDayName.DayName> {
  private static final Pattern MON_PATTERN = Pattern.compile("^m[ou]n(day)?$");
  private static final Pattern TUE_PATTERN = Pattern.compile("^t(ue|eu|oo|u|)s(day)?$");
  private static final Pattern TUE_PATTERN_1 = Pattern.compile("^tue$");
  private static final Pattern WED_PATTERN_1 = Pattern.compile("^we(dnes|nds|nns)day$");
  private static final Pattern WED_PATTERN_2 = Pattern.compile("^wed$");
  private static final Pattern THU_PATTERN_1 = Pattern.compile("^th(urs|ers)day$");
  private static final Pattern THU_PATTERN_2 = Pattern.compile("^thu$");
  private static final Pattern FRI_PATTERN = Pattern.compile("^fr[iy](day)?$");
  private static final Pattern SAT_PATTERN = Pattern.compile("^sat(t?[ue]rday)?$");
  private static final Pattern SUN_PATTERN = Pattern.compile("^su[nm](day)?$");

  public static final int DAY_SECONDS = 86400; // (24 * 60 * 60);

  public static enum DayName {
    SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY
  }

  private Calendar _currentDayStart;

  public RepeaterDayName(RepeaterDayName.DayName type) {
    super(type);
  }

  @Override
  protected Span _nextSpan(PointerType pointer) {
    int direction = (pointer == Pointer.PointerType.FUTURE) ? 1 : -1;
    if (_currentDayStart == null) {
      _currentDayStart = Time.ymd(getNow());
      _currentDayStart.add(Calendar.DAY_OF_MONTH, direction);

      int dayNum = getType().ordinal();

      while ((_currentDayStart.get(Calendar.DAY_OF_WEEK) - 1) != dayNum) {
        _currentDayStart.add(Calendar.DAY_OF_MONTH, direction);
      }
    }
    else {
      _currentDayStart.add(Calendar.DAY_OF_MONTH, direction * 7);
    }
    return new Span(_currentDayStart, Calendar.DAY_OF_MONTH, 1);
  }

  @Override
  protected Span _thisSpan(PointerType pointer) {
    if (pointer == PointerType.NONE) {
      pointer = PointerType.FUTURE;
    }
    return super.nextSpan(pointer);
  }

  @Override
  public Span getOffset(Span span, int amount, PointerType pointer) {
    throw new IllegalStateException("Not implemented.");
  }

  @Override
  public int getWidth() {
    // WARN: Does not use Calendar
    return RepeaterDayName.DAY_SECONDS;
  }

  @Override
  public String toString() {
    return super.toString() + "-dayname-" + getType();
  }

  public static RepeaterDayName scan(Token token) {
    Map<Pattern, RepeaterDayName.DayName> scanner = new HashMap<Pattern, RepeaterDayName.DayName>();
    scanner.put(RepeaterDayName.MON_PATTERN, RepeaterDayName.DayName.MONDAY);
    scanner.put(RepeaterDayName.TUE_PATTERN, RepeaterDayName.DayName.TUESDAY);
    scanner.put(RepeaterDayName.TUE_PATTERN_1, RepeaterDayName.DayName.TUESDAY);
    scanner.put(RepeaterDayName.WED_PATTERN_1, RepeaterDayName.DayName.WEDNESDAY);
    scanner.put(RepeaterDayName.WED_PATTERN_2, RepeaterDayName.DayName.WEDNESDAY);
    scanner.put(RepeaterDayName.THU_PATTERN_1, RepeaterDayName.DayName.THURSDAY);
    scanner.put(RepeaterDayName.THU_PATTERN_2, RepeaterDayName.DayName.THURSDAY);
    scanner.put(RepeaterDayName.FRI_PATTERN, RepeaterDayName.DayName.FRIDAY);
    scanner.put(RepeaterDayName.SAT_PATTERN, RepeaterDayName.DayName.SATURDAY);
    scanner.put(RepeaterDayName.SUN_PATTERN, RepeaterDayName.DayName.SUNDAY);
    for (Pattern scannerItem : scanner.keySet()) {
      if (scannerItem.matcher(token.getWord()).matches()) {
        return new RepeaterDayName(scanner.get(scannerItem));
      }
    }
    return null;
  }

} public abstract class RepeaterDayPortion<T> extends Repeater<T> {
  private static final Pattern AM_PATTERN = Pattern.compile("^ams?$");
  private static final Pattern PM_PATTERN = Pattern.compile("^pms?$");
  private static final Pattern MORNING_PATTERN = Pattern.compile("^mornings?$");
  private static final Pattern AFTERNOON_PATTERN = Pattern.compile("^afternoons?$");
  private static final Pattern EVENING_PATTERN = Pattern.compile("^evenings?$");
  private static final Pattern NIGHT_PATTERN = Pattern.compile("^(night|nite)s?$");

  private static final int FULL_DAY_SECONDS = 60 * 60 * 24;

  public static enum DayPortion {
    AM, PM, MORNING, AFTERNOON, EVENING, NIGHT
  }

  private Range _range;
  private Span _currentSpan;

  public RepeaterDayPortion(T type) {
    super(type);
    _range = createRange(type);
  }

  @Override
  protected Span _nextSpan(PointerType pointer) {
    Calendar rangeStart;
    Calendar rangeEnd;
    if (_currentSpan == null) {
      long nowSeconds = (getNow().getTimeInMillis() - Time.ymd(getNow()).getTimeInMillis()) / 1000;
      if (nowSeconds < _range.getBegin()) {
        if (pointer == Pointer.PointerType.FUTURE) {
          rangeStart = Time.cloneAndAdd(Time.ymd(getNow()), Calendar.SECOND, _range.getBegin());
        }
        else if (pointer == Pointer.PointerType.PAST) {
          rangeStart = Time.cloneAndAdd(Time.cloneAndAdd(Time.ymd(getNow()), Calendar.DAY_OF_MONTH, -1), Calendar.SECOND, _range.getBegin());
        }
        else {
          throw new IllegalArgumentException("Unable to handle pointer type " + pointer);
        }
      }
      else if (nowSeconds > _range.getBegin()) {
        if (pointer == Pointer.PointerType.FUTURE) {
          rangeStart = Time.cloneAndAdd(Time.cloneAndAdd(Time.ymd(getNow()), Calendar.DAY_OF_MONTH, 1), Calendar.SECOND, _range.getBegin());
        }
        else if (pointer == Pointer.PointerType.PAST) {
          rangeStart = Time.cloneAndAdd(Time.ymd(getNow()), Calendar.SECOND, _range.getBegin());
        }
        else {
          throw new IllegalArgumentException("Unable to handle pointer type " + pointer);
        }
      }
      else {
        if (pointer == Pointer.PointerType.FUTURE) {
          rangeStart = Time.cloneAndAdd(Time.cloneAndAdd(Time.ymd(getNow()), Calendar.DAY_OF_MONTH, 1), Calendar.SECOND, _range.getBegin());
        }
        else if (pointer == Pointer.PointerType.PAST) {
          rangeStart = Time.cloneAndAdd(Time.cloneAndAdd(Time.ymd(getNow()), Calendar.DAY_OF_MONTH, -1), Calendar.SECOND, _range.getBegin());
        }
        else {
          throw new IllegalArgumentException("Unable to handle pointer type " + pointer);
        }
      }

      _currentSpan = new Span(rangeStart, Time.cloneAndAdd(rangeStart, Calendar.SECOND, _range.getWidth()));
    }
    else {
      if (pointer == Pointer.PointerType.FUTURE) {
        // WARN: Does not use Calendar
        _currentSpan = _currentSpan.add(RepeaterDayPortion.FULL_DAY_SECONDS);
      }
      else if (pointer == Pointer.PointerType.PAST) {
        // WARN: Does not use Calendar
        _currentSpan = _currentSpan.subtract(RepeaterDayPortion.FULL_DAY_SECONDS);
      }
      else {
        throw new IllegalArgumentException("Unable to handle pointer type " + pointer);
      }
    }
    return _currentSpan;
  }

  @Override
  protected Span _thisSpan(PointerType pointer) {
    Calendar rangeStart = Time.cloneAndAdd(Time.ymd(getNow()), Calendar.SECOND, _range.getBegin());
    _currentSpan = new Span(rangeStart, Time.cloneAndAdd(rangeStart, Calendar.SECOND, _range.getWidth()));
    return _currentSpan;
  }

  @Override
  public Span getOffset(Span span, int amount, PointerType pointer) {
    setStart(span.getBeginCalendar());
    Span portionSpan = nextSpan(pointer);
    int direction = (pointer == Pointer.PointerType.FUTURE) ? 1 : -1;
    portionSpan = portionSpan.add(direction * (amount - 1) * RepeaterDay.DAY_SECONDS);
    return portionSpan;
  }

  @Override
  public int getWidth() {
    if (_range == null) {
      throw new IllegalStateException("Range has not been set");
    }
    int width;
    if (_currentSpan != null) {
      width = (int) _currentSpan.getWidth();
    }
    else {
      width = _getWidth(_range);
    }
    return width;
  }
  
  protected abstract int _getWidth(Range range);
  
  protected abstract Range createRange(T type);

  @Override
  public String toString() {
    return super.toString() + "-dayportion-" + getType();
  }

  public static EnumRepeaterDayPortion scan(Token token) {
    Map<Pattern, RepeaterDayPortion.DayPortion> scanner = new HashMap<Pattern, RepeaterDayPortion.DayPortion>();
    scanner.put(RepeaterDayPortion.AM_PATTERN, RepeaterDayPortion.DayPortion.AM);
    scanner.put(RepeaterDayPortion.PM_PATTERN, RepeaterDayPortion.DayPortion.PM);
    scanner.put(RepeaterDayPortion.MORNING_PATTERN, RepeaterDayPortion.DayPortion.MORNING);
    scanner.put(RepeaterDayPortion.AFTERNOON_PATTERN, RepeaterDayPortion.DayPortion.AFTERNOON);
    scanner.put(RepeaterDayPortion.EVENING_PATTERN, RepeaterDayPortion.DayPortion.EVENING);
    scanner.put(RepeaterDayPortion.NIGHT_PATTERN, RepeaterDayPortion.DayPortion.NIGHT);
    for (Pattern scannerItem : scanner.keySet()) {
      if (scannerItem.matcher(token.getWord()).matches()) {
        return new EnumRepeaterDayPortion(scanner.get(scannerItem));
      }
    }
    return null;
  }

} public class RepeaterMonthName extends Repeater<RepeaterMonthName.MonthName> {
  private static final Pattern JAN_PATTERN = Pattern.compile("^jan\\.?(uary)?$");
  private static final Pattern FEB_PATTERN = Pattern.compile("^feb\\.?(ruary)?$");
  private static final Pattern MAR_PATTERN = Pattern.compile("^mar\\.?(ch)?$");
  private static final Pattern APR_PATTERN = Pattern.compile("^apr\\.?(il)?$");
  private static final Pattern MAY_PATTERN = Pattern.compile("^may$");
  private static final Pattern JUN_PATTERN = Pattern.compile("^jun\\.?e?$");
  private static final Pattern JUL_PATTERN = Pattern.compile("^jul\\.?y?$");
  private static final Pattern AUG_PATTERN = Pattern.compile("^aug\\.?(ust)?$");
  private static final Pattern SEP_PATTERN = Pattern.compile("^sep\\.?(t\\.?|tember)?$");
  private static final Pattern OCT_PATTERN = Pattern.compile("^oct\\.?(ober)?$");
  private static final Pattern NOV_PATTERN = Pattern.compile("^nov\\.?(ember)?$");
  private static final Pattern DEC_PATTERN = Pattern.compile("^dec\\.?(ember)?$");

  private static final int MONTH_SECONDS = 2592000; // 30 * 24 * 60 * 60

  public static enum MonthName {
    _ZERO_MONTH, JANUARY, FEBRUARY, MARCH, APRIL, MAY, JUNE, JULY, AUGUST, SEPTEMBER, OCTOBER, NOVEMBER, DECEMBER
  }

  private Calendar _currentMonthBegin;

  public RepeaterMonthName(MonthName type) {
    super(type);
  }
  
  public int getIndex() {
    return getType().ordinal();
  }

  @Override
  protected Span _nextSpan(PointerType pointer) {
    if (_currentMonthBegin == null) {
      int targetMonth = getType().ordinal();
      int nowMonth = getNow().get(Calendar.MONTH) + 1;
      if (pointer == PointerType.FUTURE) {
        if (nowMonth < targetMonth) {
          _currentMonthBegin = Time.y(getNow(), targetMonth);
        }
        else if (nowMonth > targetMonth) {
          _currentMonthBegin = Time.cloneAndAdd(Time.y(getNow(), targetMonth), Calendar.YEAR, 1);
        }
      }
      else if (pointer == PointerType.NONE) {
        if (nowMonth <= targetMonth) {
          _currentMonthBegin = Time.y(getNow(), targetMonth);
        }
        else if (nowMonth > targetMonth) {
          _currentMonthBegin = Time.cloneAndAdd(Time.y(getNow(), targetMonth), Calendar.YEAR, 1);
        }
      }
      else if (pointer == PointerType.PAST) {
        if (nowMonth > targetMonth) {
          _currentMonthBegin = Time.y(getNow(), targetMonth);
        }
        else if (nowMonth <= targetMonth) {
          _currentMonthBegin = Time.cloneAndAdd(Time.y(getNow(), targetMonth), Calendar.YEAR, -1);
        }
      }
      else {
        throw new IllegalArgumentException("Unable to handle pointer " + pointer + ".");
      }
      if (_currentMonthBegin == null) {
        throw new IllegalStateException("Current month should be set by now.");
      }
    }
    else {
      if (pointer == PointerType.FUTURE) {
        _currentMonthBegin = Time.cloneAndAdd(_currentMonthBegin, Calendar.YEAR, 1);
      }
      else if (pointer == PointerType.PAST) {
        _currentMonthBegin = Time.cloneAndAdd(_currentMonthBegin, Calendar.YEAR, -1);
      }
      else {
        throw new IllegalArgumentException("Unable to handle pointer " + pointer + ".");
      }
    }

    return new Span(_currentMonthBegin, Calendar.MONTH, 1);
  }

  @Override
  protected Span _thisSpan(PointerType pointer) {
    Span span;
    if (pointer == Pointer.PointerType.PAST) {
      span = nextSpan(pointer);
    }
    else if (pointer == Pointer.PointerType.FUTURE || pointer == Pointer.PointerType.NONE) {
      span = nextSpan(Pointer.PointerType.NONE);
    }
    else {
      throw new IllegalArgumentException("Unable to handle pointer " + pointer + ".");
    }
    return span;
  }

  @Override
  public Span getOffset(Span span, int amount, PointerType pointer) {
    throw new IllegalStateException("Not implemented.");
  }

  @Override
  public int getWidth() {
    // WARN: Does not use Calendar
    return RepeaterMonthName.MONTH_SECONDS;
  }

  @Override
  public String toString() {
    return super.toString() + "-monthname-" + getType();
  }

  public static RepeaterMonthName scan(Token token) {
    Map<Pattern, RepeaterMonthName.MonthName> scanner = new HashMap<Pattern, RepeaterMonthName.MonthName>();
    scanner.put(RepeaterMonthName.JAN_PATTERN, RepeaterMonthName.MonthName.JANUARY);
    scanner.put(RepeaterMonthName.FEB_PATTERN, RepeaterMonthName.MonthName.FEBRUARY);
    scanner.put(RepeaterMonthName.MAR_PATTERN, RepeaterMonthName.MonthName.MARCH);
    scanner.put(RepeaterMonthName.APR_PATTERN, RepeaterMonthName.MonthName.APRIL);
    scanner.put(RepeaterMonthName.MAY_PATTERN, RepeaterMonthName.MonthName.MAY);
    scanner.put(RepeaterMonthName.JUN_PATTERN, RepeaterMonthName.MonthName.JUNE);
    scanner.put(RepeaterMonthName.JUL_PATTERN, RepeaterMonthName.MonthName.JULY);
    scanner.put(RepeaterMonthName.AUG_PATTERN, RepeaterMonthName.MonthName.AUGUST);
    scanner.put(RepeaterMonthName.SEP_PATTERN, RepeaterMonthName.MonthName.SEPTEMBER);
    scanner.put(RepeaterMonthName.OCT_PATTERN, RepeaterMonthName.MonthName.OCTOBER);
    scanner.put(RepeaterMonthName.NOV_PATTERN, RepeaterMonthName.MonthName.NOVEMBER);
    scanner.put(RepeaterMonthName.DEC_PATTERN, RepeaterMonthName.MonthName.DECEMBER);
    for (Pattern scannerItem : scanner.keySet()) {
      if (scannerItem.matcher(token.getWord()).matches()) {
        return new RepeaterMonthName(scanner.get(scannerItem));
      }
    }
    return null;
  }

} public class RepeaterTime extends Repeater<Tick> {
  private static final Pattern TIME_PATTERN = Pattern.compile("^\\d{1,2}(:?\\d{2})?([\\.:]?\\d{2})?$");

  public RepeaterTime(String time) {
    super(null);
    String t = time.replaceAll(":", "");
    Tick type;
    int length = t.length();
    if (length <= 2) {
      int hours = Integer.parseInt(t);
      int hoursInSeconds = hours * 60 * 60;
      if (hours == 12) {
        type = new Tick(0 * 60 * 60, true);
      }
      else {
        type = new Tick(hoursInSeconds, true);
      }
    }
    else if (length == 3) {
      int hoursInSeconds = Integer.parseInt(t.substring(0, 1)) * 60 * 60;
      int minutesInSeconds = Integer.parseInt(t.substring(1)) * 60;
      type = new Tick(hoursInSeconds + minutesInSeconds, true);
    }
    else if (length == 4) {
      boolean ambiguous = (time.contains(":") && Integer.parseInt(t.substring(0, 1)) != 0 && Integer.parseInt(t.substring(0, 2)) <= 12);
      int hours = Integer.parseInt(t.substring(0, 2));
      int hoursInSeconds = hours * 60 * 60;
      int minutesInSeconds = Integer.parseInt(t.substring(2)) * 60;
      if (hours == 12) {
        type = new Tick(0 * 60 * 60 + minutesInSeconds, ambiguous);
      }
      else {
        type = new Tick(hoursInSeconds + minutesInSeconds, ambiguous);
      }
    }
    else if (length == 5) {
      int hoursInSeconds = Integer.parseInt(t.substring(0, 1)) * 60 * 60;
      int minutesInSeconds = Integer.parseInt(t.substring(1, 3)) * 60;
      int seconds = Integer.parseInt(t.substring(3));
      type = new Tick(hoursInSeconds + minutesInSeconds + seconds, true);
    }
    else if (length == 6) {
      boolean ambiguous = (time.contains(":") && Integer.parseInt(t.substring(0, 1)) != 0 && Integer.parseInt(t.substring(0, 2)) <= 12);
      int hours = Integer.parseInt(t.substring(0, 2));
      int hoursInSeconds = hours * 60 * 60;
      int minutesInSeconds = Integer.parseInt(t.substring(2, 4)) * 60;
      int seconds = Integer.parseInt(t.substring(4, 6));
      //type = new Tick(hoursInSeconds + minutesInSeconds + seconds, ambiguous);
      if (hours == 12) {
        type = new Tick(0 * 60 * 60 + minutesInSeconds + seconds, ambiguous);
      }
      else {
        type = new Tick(hoursInSeconds + minutesInSeconds + seconds, ambiguous);
      }
    }
    else {
      throw new IllegalArgumentException("Time cannot exceed six digits");
    }
    setType(type);
  }

  private Calendar _currentTime;

  @Override
  protected Span _nextSpan(PointerType pointer) {
    int halfDay = RepeaterDay.DAY_SECONDS / 2;
    int fullDay = RepeaterDay.DAY_SECONDS;

    Calendar now = getNow();
    Tick tick = getType();
    boolean first = false;
    if (_currentTime == null) {
      first = true;
      Calendar midnight = Time.ymd(now);
      Calendar yesterdayMidnight = Time.cloneAndAdd(midnight, Calendar.SECOND, -fullDay);
      Calendar tomorrowMidnight = Time.cloneAndAdd(midnight, Calendar.SECOND, fullDay);
      boolean done = false;
      if (pointer == Pointer.PointerType.FUTURE) {
        if (tick.isAmbiguous()) {
          List<Calendar> futureDates = new LinkedList<Calendar>();
          futureDates.add(Time.cloneAndAdd(midnight, Calendar.SECOND, tick.intValue()));
          futureDates.add(Time.cloneAndAdd(midnight, Calendar.SECOND, halfDay + tick.intValue()));
          futureDates.add(Time.cloneAndAdd(tomorrowMidnight, Calendar.SECOND, tick.intValue()));
          for (Calendar futureDate : futureDates) {
            if (futureDate.after(now) || futureDate.equals(now)) {
              _currentTime = futureDate;
              done = true;
              break;
            }
          }
        }
        else {
          List<Calendar> futureDates = new LinkedList<Calendar>();
          futureDates.add(Time.cloneAndAdd(midnight, Calendar.SECOND, tick.intValue()));
          futureDates.add(Time.cloneAndAdd(tomorrowMidnight, Calendar.SECOND, tick.intValue()));
          for (Calendar futureDate : futureDates) {
            if (futureDate.after(now) || futureDate.equals(now)) {
              _currentTime = futureDate;
              done = true;
              break;
            }
          }
        }
      }
      else {
        if (tick.isAmbiguous()) {
          List<Calendar> pastDates = new LinkedList<Calendar>();
          pastDates.add(Time.cloneAndAdd(midnight, Calendar.SECOND, halfDay + tick.intValue()));
          pastDates.add(Time.cloneAndAdd(midnight, Calendar.SECOND, tick.intValue()));
          pastDates.add(Time.cloneAndAdd(yesterdayMidnight, Calendar.SECOND, tick.intValue() * 2));
          for (Calendar pastDate : pastDates) {
            if (pastDate.before(now) || pastDate.equals(now)) {
              _currentTime = pastDate;
              done = true;
              break;
            }
          }
        }
        else {
          List<Calendar> pastDates = new LinkedList<Calendar>();
          pastDates.add(Time.cloneAndAdd(midnight, Calendar.SECOND, tick.intValue()));
          pastDates.add(Time.cloneAndAdd(yesterdayMidnight, Calendar.SECOND, tick.intValue()));
          for (Calendar pastDate : pastDates) {
            if (pastDate.before(now) || pastDate.equals(now)) {
              _currentTime = pastDate;
              done = true;
              break;
            }
          }
        }
      }

      if (!done && _currentTime == null) {
        throw new IllegalStateException("Current time cannot be null at this point.");
      }
    }

    if (!first) {
      int increment = (tick.isAmbiguous()) ? halfDay : fullDay;
      int direction = (pointer == Pointer.PointerType.FUTURE) ? 1 : -1;
      _currentTime.add(Calendar.SECOND, direction * increment);
    }

    return new Span(_currentTime, Time.cloneAndAdd(_currentTime, Calendar.SECOND, getWidth()));
  }

  @Override
  protected Span _thisSpan(PointerType pointer) {
    if (pointer == Pointer.PointerType.NONE) {
      pointer = Pointer.PointerType.FUTURE;
    }
    return nextSpan(pointer);
  }

  @Override
  public Span getOffset(Span span, int amount, PointerType pointer) {
    throw new IllegalStateException("Not implemented.");
  }

  @Override
  public int getWidth() {
    return 1;
  }

  @Override
  public String toString() {
    return super.toString() + "-time-" + getType();
  }

  public static RepeaterTime scan(Token token, List<Token> tokens, Options options) {
    if (RepeaterTime.TIME_PATTERN.matcher(token.getWord()).matches()) {
      return new RepeaterTime(token.getWord());
    }
    Integer intStrValue = StringUtils.integerValue(token.getWord());
    if (intStrValue != null) {
      return new RepeaterTime(intStrValue.toString());
    }
    return null;
  }
} public class RepeaterYear extends RepeaterUnit {
  private Calendar _currentYearStart;

  @Override
  protected Span _nextSpan(PointerType pointer) {
    if (_currentYearStart == null) {
      if (pointer == PointerType.FUTURE) {
        _currentYearStart = Time.cloneAndAdd(Time.y(getNow()), Calendar.YEAR, 1);
      }
      else if (pointer == PointerType.PAST) {
        _currentYearStart = Time.cloneAndAdd(Time.y(getNow()), Calendar.YEAR, -1);
      }
      else {
        throw new IllegalArgumentException("Unable to handle pointer " + pointer + ".");
      }
    }
    else {
      int direction = (pointer == Pointer.PointerType.FUTURE) ? 1 : -1;
      _currentYearStart.add(Calendar.YEAR, direction);
    }

    return new Span(_currentYearStart, Calendar.YEAR, 1);
  }

  @Override
  protected Span _thisSpan(PointerType pointer) {
    Calendar yearStart;
    Calendar yearEnd;
    if (pointer == PointerType.FUTURE) {
      yearStart = Time.cloneAndAdd(Time.ymd(getNow()), Calendar.DAY_OF_MONTH, 1);
      yearEnd = Time.cloneAndAdd(Time.yJan1(getNow()), Calendar.YEAR, 1);
    }
    else if (pointer == PointerType.PAST) {
      yearStart = Time.yJan1(getNow());
      yearEnd = Time.ymd(getNow());
    }
    else if (pointer == PointerType.NONE) {
      yearStart = Time.yJan1(getNow());
      yearEnd = Time.cloneAndAdd(Time.yJan1(getNow()), Calendar.YEAR, 1);
    }
    else {
      throw new IllegalArgumentException("Unable to handle pointer " + pointer + ".");
    }
    return new Span(yearStart, yearEnd);
  }

  @Override
  public Span getOffset(Span span, int amount, Pointer.PointerType pointer) {
    int direction = (pointer == Pointer.PointerType.FUTURE) ? 1 : -1;
    Calendar newBegin = Time.cloneAndAdd(span.getBeginCalendar(), Calendar.YEAR, amount * direction);
    Calendar newEnd = Time.cloneAndAdd(span.getEndCalendar(), Calendar.YEAR, amount * direction);
    return new Span(newBegin, newEnd);
  }

  @Override
  public int getWidth() {
    // WARN: Does not use Calendar
    return (365 * 24 * 60 * 60);
  }

  @Override
  public String toString() {
    return super.toString() + "-year";
  }
} public class Pointer extends Tag<Pointer.PointerType> {
  private static final Pattern IN_PATTERN = Pattern.compile("\\bin\\b");
  private static final Pattern FUTURE_PATTERN = Pattern.compile("\\bfuture\\b");
  private static final Pattern PAST_PATTERN = Pattern.compile("\\bpast\\b");

  public enum PointerType {
    PAST, FUTURE, NONE
  }
  
  public Pointer(Pointer.PointerType type) {
    super(type);
  }

  public static List<Token> scan(List<Token> tokens, Options options) {
    for (Token token : tokens) {
      Pointer t = Pointer.scanForAll(token, options);
      if (t != null) {
        token.tag(t);
      }
    }
    return tokens;
  }

  public static Pointer scanForAll(Token token, Options options) {
    Map<Pattern, Pointer.PointerType> scanner = new HashMap<Pattern, Pointer.PointerType>();
    scanner.put(Pointer.PAST_PATTERN, Pointer.PointerType.PAST);
    scanner.put(Pointer.FUTURE_PATTERN, Pointer.PointerType.FUTURE);
    scanner.put(Pointer.IN_PATTERN, Pointer.PointerType.FUTURE);
    for (Pattern scannerItem : scanner.keySet()) {
      if (scannerItem.matcher(token.getWord()).matches()) { 
        return new Pointer(scanner.get(scannerItem));
      }
    }
    return null;
  }

  @Override
  public String toString() {
    return "pointer-" + getType();
  }
} public class Scalar extends Tag<Integer> {
  private static final Pattern SCALAR_PATTERN = Pattern.compile("^\\d*$");
  public static Set<String> TIMES = new HashSet<String>();

  static {
    Scalar.TIMES.add("am");
    Scalar.TIMES.add("pm");
    Scalar.TIMES.add("morning");
    Scalar.TIMES.add("afternoon");
    Scalar.TIMES.add("evening");
    Scalar.TIMES.add("night");
  }

  public Scalar(Integer type) {
    super(type);
  }

  public static List<Token> scan(List<Token> tokens, Options options) {
    for (int i = 0; i < tokens.size(); i++) {
      Token token = tokens.get(i);
      Token postToken = null;
      if (i < tokens.size() - 1) {
        postToken = tokens.get(i + 1);
      }
      Scalar t;
      t = Scalar.scan(token, postToken, options);
      if (t != null) {
        token.tag(t);
      }
      t = ScalarDay.scan(token, postToken, options);
      if (t != null) {
        token.tag(t);
      }
      t = ScalarMonth.scan(token, postToken, options);
      if (t != null) {
        token.tag(t);
      }
      t = ScalarYear.scan(token, postToken, options);
      if (t != null) {
        token.tag(t);
      }
    }
    return tokens;
  }

  public static Scalar scan(Token token, Token postToken, Options options) {
    if (Scalar.SCALAR_PATTERN.matcher(token.getWord()).matches()) {
      if (token.getWord() != null && token.getWord().length() > 0 && !(postToken != null && Scalar.TIMES.contains(postToken.getWord()))) {
        return new Scalar(Integer.valueOf(token.getWord()));
      }
    }
    else {
      Integer intStrValue = StringUtils.integerValue(token.getWord());
      if (intStrValue != null) {
        return new Scalar(intStrValue);
      }
    }
    return null;
  }

  @Override
  public String toString() {
    return "scalar";
  }
} public class ScalarDay extends Scalar {
  private static final Pattern DAY_PATTERN = Pattern.compile("^\\d\\d?$");

  public ScalarDay(Integer type) {
    super(type);
  }

  @Override
  public String toString() {
    return super.toString() + "-day-" + getType();
  }

  public static ScalarDay scan(Token token, Token postToken, Options options) {
    if (ScalarDay.DAY_PATTERN.matcher(token.getWord()).matches()) {
      int scalarValue = Integer.parseInt(token.getWord());
      if (!(scalarValue > 31 || (postToken != null && Scalar.TIMES.contains(postToken.getWord())))) {
        return new ScalarDay(Integer.valueOf(scalarValue));
      }
    }
    return null;
  }
} public class Time {
  public static Calendar construct(int year, int month) {
    if (year <= 1900) {
      throw new IllegalArgumentException("Illegal year '" + year + "'");
    }
    Calendar cal = Calendar.getInstance();
    cal.clear();
    cal.set(Calendar.YEAR, year);
    cal.set(Calendar.MONTH, month - 1);
    return cal;
  }

  public static Calendar construct(int year, int month, int day) {
    Calendar cal = Time.construct(year, month);
    cal.set(Calendar.DAY_OF_MONTH, day);
    return cal;
  }

  public static Calendar construct(int year, int month, int day, int hour) {
    Calendar cal = Time.construct(year, month, day);
    cal.set(Calendar.HOUR_OF_DAY, hour);
    return cal;
  }

  public static Calendar construct(int year, int month, int day, int hour, int minute) {
    Calendar cal = Time.construct(year, month, day, hour);
    cal.set(Calendar.MINUTE, minute);
    return cal;
  }

  public static Calendar construct(int year, int month, int day, int hour, int minute, int second) {
    Calendar cal = Time.construct(year, month, day, hour, minute);
    cal.set(Calendar.SECOND, second);
    return cal;
  }

  public static Calendar construct(int year, int month, int day, int hour, int minute, int second, int millisecond) {
    Calendar cal = Time.construct(year, month, day, hour, minute, second);
    cal.set(Calendar.MILLISECOND, millisecond);
    return cal;
  }

  public static Calendar y(Calendar basis) {
    Calendar clone = Calendar.getInstance();
    clone.clear();
    clone.set(Calendar.YEAR, basis.get(Calendar.YEAR));
    return clone;
  }

  public static Calendar yJan1(Calendar basis) {
    Calendar clone = Time.y(basis, 1, 1);
    return clone;
  }

  public static Calendar y(Calendar basis, int month) {
    Calendar clone = Time.y(basis);
    clone.set(Calendar.MONTH, month - 1);
    return clone;
  }

  public static Calendar y(Calendar basis, int month, int day) {
    Calendar clone = Time.y(basis, month);
    clone.set(Calendar.DAY_OF_MONTH, day);
    return clone;
  }

  public static Calendar ym(Calendar basis) {
    Calendar clone = Time.y(basis);
    clone.set(Calendar.MONTH, basis.get(Calendar.MONTH));
    return clone;
  }

  public static Calendar ymd(Calendar basis) {
    Calendar clone = Time.ym(basis);
    clone.set(Calendar.DAY_OF_MONTH, basis.get(Calendar.DAY_OF_MONTH));
    return clone;
  }

  public static Calendar ymdh(Calendar basis) {
    Calendar clone = Time.ymd(basis);
    clone.set(Calendar.HOUR_OF_DAY, basis.get(Calendar.HOUR_OF_DAY));
    return clone;
  }

  public static Calendar ymdhm(Calendar basis) {
    Calendar clone = Time.ymdh(basis);
    clone.set(Calendar.MINUTE, basis.get(Calendar.MINUTE));
    return clone;
  }

  public static Calendar cloneAndAdd(Calendar basis, int field, long amount) {
    Calendar next = (Calendar) basis.clone();
    next.add(field, (int) amount);
    return next;
  }
} public class Token {
  private String _word;
  private List<Tag<?>> _tags;

  public Token(String word) {
    _word = word;
    _tags = new LinkedList<Tag<?>>();
  }

  public String getWord() {
    return _word;
  }

  /**
   * Tag this token with the specified tag
   */
  public void tag(Tag<?> newTag) {
    _tags.add(newTag);
  }

  /**
   * Remove all tags of the given class
   */
  public void untag(Class<?> tagClass) {
    Iterator<Tag<?>> tagIter = _tags.iterator();
    while (tagIter.hasNext()) {
      Tag<?> tag = tagIter.next();
      if (tagClass.isInstance(tag)) {
        tagIter.remove();
      }
    }
  }

  /**
   * Return true if this token has any tags
   */
  public boolean isTagged() {
    return !_tags.isEmpty();
  }

  /**
   * Return the Tag that matches the given class
   */
  @SuppressWarnings("unchecked")
  public <T extends Tag> T getTag(Class<T> tagClass) {
    List<T> matches = getTags(tagClass);
    T matchingTag = null;
    if (matches.size() > 0) {
      matchingTag = matches.get(0);
    }
//    if (matches.size() >= 2) {
//      throw new IllegalStateException("Multiple identical tags found (" + matches + ")");
//    }
//    else if (matches.size() == 1) {
//      matchingTag = matches.get(0);
//    }
    return matchingTag;
  }

  public List<Tag<?>> getTags() {
    return _tags;
  }
  
  /**
   * Return the Tag that matches the given class
   */
  @SuppressWarnings("unchecked")
  public <T extends Tag<?>> List<T> getTags(Class<T> tagClass) {
    List<T> matches = new LinkedList<T>();
    Iterator<Tag<?>> tagIter = _tags.iterator();
    while (tagIter.hasNext()) {
      Tag<?> tag = tagIter.next();
      if (tagClass.isInstance(tag)) {
        matches.add((T)tag);
      }
    }
    return matches;
  }

  @Override
  public String toString() {
    return _word + " " + _tags;
  }
}
import java.util.Calendar;
import java.util.LinkedList;
import java.util.List;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import com.mdimension.jchronic.handlers.DummyHandler;
import com.mdimension.jchronic.handlers.Handler;
import com.mdimension.jchronic.handlers.HandlerTypePattern;
import com.mdimension.jchronic.handlers.TagPattern;
import com.mdimension.jchronic.repeaters.EnumRepeaterDayPortion;
import com.mdimension.jchronic.repeaters.Repeater;
import com.mdimension.jchronic.repeaters.RepeaterDayName;
import com.mdimension.jchronic.repeaters.RepeaterDayPortion;
import com.mdimension.jchronic.repeaters.RepeaterMonthName;
import com.mdimension.jchronic.repeaters.RepeaterTime;
import com.mdimension.jchronic.repeaters.RepeaterYear;
import com.mdimension.jchronic.tags.Pointer;
import com.mdimension.jchronic.tags.Scalar;
import com.mdimension.jchronic.tags.ScalarDay;
import com.mdimension.jchronic.utils.Time;
import com.mdimension.jchronic.utils.Token;
package com.mdimension.jchronic;




@RunWith(JUnit4.class)
public class HandlerTestCase {
  private Calendar _now;
  
  @Before
  public void setUp() throws Exception {
    _now = Time.construct(2006, 8, 16, 14, 0, 0, 0);
  }

  @Test
  public void testHandlerClass1() {
    Handler handler = new Handler(new DummyHandler(), new TagPattern(Repeater.class));